目次
1、このポインタ
1.1 はじめに
まずコードの一部を見てください
#include <iostream>
class Person
{
public:
void Init(const std::string& name, int age)
{
_name = name;
_age = age;
}
void Print()
{
std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
}
private:
std::string _name;
int _age;
};
int main()
{
Person p1, p2;
p1.Init("John", 30);
p2.Init("Alice", 25);
p1.Print(); // Output: Name: John, Age: 30
p2.Print(); // Output: Name: Alice, Age: 25
return 0;
}
1.2 質問
Person クラスにはInitとPrint という2 つのメンバー関数があり、関数本体では異なるオブジェクト間の区別がないため、どのオブジェクトが呼び出しているかを区別するにはどうすればよいでしょうか。
C++ 設計者は、この問題を解決するためにthis ポインターを使用することを提案しています。メンバー関数を呼び出すと、C++ コンパイラーは内部で隠しポインター パラメーター、つまりポインターを各非静的メンバー関数に追加します。このポインタは、現在のオブジェクトのアドレスへの定数ポインタであり、メンバー関数を呼び出したオブジェクトを指します。関数本体では、メンバー変数に対するすべての操作にポインターを介してアクセスします。すべての操作がユーザーに対して透過的であるというだけです。つまり、ユーザーが操作を渡す必要がなく、コンパイラーが自動的に操作を完了します。this
this
this
図に示すように、クラスのメンバー関数を定義する場合は、上記の 1 番目の形式で定義する必要があります。2 番目の形式は、コンパイラーがパラメーター リスト内の非静的メンバー関数を呼び出すメンバーに定数ポインターを自動的に渡すことです。上図は、読者が処理を体験できるように、関数内で直接メンバー属性を使用できるようにしたもので、関数のパラメータ名とメンバー属性が競合する場合は、 this->member 属性という方法で区別できます。 !
メンバー関数では、
this
ポインターを使用して、現在のオブジェクトのメンバー変数およびメンバー関数にアクセスできます。たとえば、クラス内で呼び出されるメンバー変数と関数がある場合value
、それを使用して、this->value
関数の代わりにメンバー変数がアクセスされることを明確に示すことができます。さらに、メンバー関数で返すことにより*this
、連鎖呼び出しを実装でき、コードの読みやすさと単純さが向上します。
1.3 特徴
this
ポインターの型は です。つまり、ポインターは現在のオブジェクトのアドレスを指しており、他のオブジェクトを指すことは許可されていないため类类型* const
、メンバー関数で値を割り当てることはできません。this
this
ポインターはメンバー関数内でのみ使用でき、クラスの非メンバー関数やグローバル関数では使用できません。this
ポインタは本質的にメンバ関数の暗黙的なパラメータであり、オブジェクトがメンバ関数を呼び出すと、コンパイラはオブジェクトのアドレスを実際のパラメータとしてポインタに渡しますthis
。したがって、オブジェクト自体はthis
ポインターを格納しません。
2 番目に、コンストラクター
空のクラス: クラスにはメンバー プロパティとメンバー関数がありません。
C++ では、クラス内で定義しない場合、または明示的に定義しない場合、クラスはいくつかのデフォルトのメンバー関数を自動的に生成します。これらのデフォルトのメンバー関数には次のものがあります。
- デフォルトのコンストラクター
- デフォルトのデストラクター
- デフォルトのコピーコンストラクター
- デフォルトのコピー代入演算子
- デフォルトの移動コンストラクター
- デフォルトの移動代入演算子
この記事では、主に最初の 4 つの機能を紹介し、その後、その他の機能を紹介します。
2.1 コンセプト
上記のコードを参照して、説明を拡張できます。
class Person
{
public:
void Init(const std::string& name, int age)
{
_name = name;
_age = age;
}
void Print()
{
std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
}
private:
std::string _name;
int _age;
};
オブジェクトを作成する際には、毎回 Init 関数を呼び出してオブジェクトのプロパティに値を代入する必要がありますが、オブジェクトを作成するたびにこのメソッドを呼び出して値を代入するのは少々面倒に思えます。オブジェクトの作成時に情報を設定できますか? これによりコンストラクターが作成されます。
- 機能:オブジェクトの作成時にメンバー変数のデフォルト値を初期化するために使用されます。
- 使用法:クラス オブジェクトを作成するときに、コンストラクターを明示的に指定しない場合、コンパイラーはデフォルトのコンストラクターを自動的に生成します。デフォルトのコンストラクターにはパラメーターがなく、メンバー変数を対応する型のデフォルト値に初期化します (たとえば、数値型の場合は 0、ポインター型の場合は nullptr、クラス オブジェクトのメンバーは初期化のために独自のコンストラクターを呼び出します)。 。実装はコンパイラによって異なる場合があります。一部のコンパイラは、ランダムな値である組み込み型を処理しません。クラス オブジェクトのメンバーは、コンストラクターを呼び出します。
- コンストラクターは、クラス名と同じ名前を持つ特別なメンバー関数であり、クラス型オブジェクトの作成時にコンパイラーによって自動的に呼び出され、各データ メンバーが適切な初期値を持つようにし、生涯に 1 回だけ呼び出されます。オブジェクトのサイクル。
2.2 特徴
コンストラクターは特別なメンバー関数です。コンストラクターの名前はコンストラクターと呼ばれていますが、コンストラクターの主なタスクはオブジェクトを作成するためのスペースを作成することではなく、オブジェクトを初期化し、メンバーのプロパティを初期化することであることに注意してください。オブジェクトの中で。
特徴:
- 1. 関数名はクラス名と同じです。
- 2. 戻り値はありません。
- 3. コンパイラは、オブジェクトがインスタンス化されるときに、対応するコンストラクターを自動的に呼び出します。
- 4. コンストラクターはオーバーロードできます。
2.3 文法
#include <iostream>
#include <string>
class Person
{
public:
// 1.无参构造函数
Person()
{}
// 2.带参构造函数
Person(const std::string& name, int age)
{
_name = name;
_age = age;
}
// 3.打印个人信息
void PrintInfo()
{
std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
}
private:
std::string _name;
int _age;
};
上記のコードから、コンストラクターをパラメーターなしのコンストラクターとパラメーター化されたコンストラクターに一時的に分割できます。これら 2 つのパラメーターを使用してオブジェクトを初期化する方法を見てみましょう。
int main()
{
// 调用无参构造函数创建对象
Person p1;
p1.PrintInfo();
// 调用带参构造函数创建对象
Person p2("John", 30);
p2.PrintInfo(); // Output: Name: John, Age: 30
return 0;
}
引数なしの構築を使用してオブジェクトを初期化する場合は、クラス名 + オブジェクト名 を直接使用できます。パラメータ構築を使用する場合は、対応するパラメータを渡してメンバー プロパティを初期化する必要があります。
思い出させる
Person person();
引数のない構造でオブジェクトをインスタンス化する場合、オブジェクトの後に () を追加しないでください。これにより、コンパイラは、これが引数のない構造を使用したオブジェクトのインスタンス化なのか、それとも戻り値を宣言する関数なのかを識別できなくなります。 value は Person タイプです。これを使用しないことをお勧めします。!!
クラスを定義するときにコンストラクターを積極的に作成しない場合、システムはデフォルトのコンストラクター、つまり引数のないコンストラクターを自動的に生成します。ユーザーがコンパイラーが生成しなくなることを明示的に定義すると、つまりユーザーがパラメーターを実装するかパラメーターを実装しないと、コンパイラーはデフォルト (パラメーターなし) コンストラクターを実装しなくなります。
上記のコードの引数なしコンストラクターに注釈を付けて、引数-引数コンストラクターのみを残す場合、オブジェクトは引数-引数コンストラクターを呼び出すことによってのみインスタンス化でき、引数なしコンストラクターは使用できません。
2.4 注意点
パラメーターなしのコンストラクターとデフォルト コンストラクターは両方ともデフォルト コンストラクターと呼ばれ、デフォルト コンストラクターは 1 つだけです。注: 引数のないコンストラクタ、完全なデフォルト コンストラクタ、およびデフォルトでコンパイラによって生成されるように記述されていないコンストラクタはすべて、デフォルト コンストラクタと見なすことができます。
class Date {
public:
Date() {
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
// 以下测试函数能通过编译吗?
void Test() {
Date d1;
}
答えはいいえだ。
現時点では、引数のないコンストラクターと完全なデフォルトのコンストラクターがこのコード行のオブジェクトをインスタンス化する可能性があるため、あいまいさが生じてコンパイルに失敗する可能性があります。!!
3. デストラクター
3.1 コンセプト
デストラクターは C++ の特別なメンバー関数であり、オブジェクトが破棄されたときにリソースをクリーンアップして解放するために使用されます。その名前には、クラス名の前にチルダ (~) が付加されます。たとえば、クラス名が の場合、
ClassName
デストラクターの名前は です~ClassName
。デストラクターの役割は、オブジェクトのその後を処理することです。オブジェクトのライフサイクルが終了すると (オブジェクトがスコープ外になる、明示的に削除される、プログラムが終了するなど)、デストラクターは自動的に呼び出されます。 。
3.2 特徴
デストラクターには次の特徴があります。
- デストラクターには戻り値 ( を含む) がなく
void
、パラメーターもありません。 - クラスにはデストラクターを 1 つだけ持つことができ、オーバーロードすることはできません。
- デストラクターを明示的に定義しない場合、コンパイラーはデフォルトのデストラクターを自動的に生成します。
- クラス内に動的に割り当てられたリソース (ヒープ上のメモリ、ファイル ハンドルなど) がある場合、メモリ リークやリソース リークを避けるために、これらのリソースをデストラクタで解放する必要があります。
3.3 例
#include <iostream>
class MyClass {
public:
// 构造函数
MyClass() {
std::cout << "Constructor called." << std::endl;
}
// 析构函数
~MyClass() {
std::cout << "Destructor called." << std::endl;
}
};
int main() {
std::cout << "Creating object..." << std::endl;
MyClass obj; // 创建对象,调用构造函数
std::cout << "Object will be destroyed..." << std::endl;
// 在这里,obj超出了作用域,对象的生命周期结束,析构函数被自动调用
return 0;
}
Creating object...
Constructor called.
Object will be destroyed...
Destructor called.
これは、オブジェクトの作成時と破棄時に、それぞれオブジェクトのコンストラクターとデストラクターが呼び出されることを証明しています。デストラクターを呼び出すと、オブジェクトが破棄されるときに必要なクリーンアップ作業が確実に完了し、リソースが解放され、リソース リークが回避されます。
注意:デストラクターを手動で作成しない場合、システムは自動的にデストラクターを生成しますが、システム独自のデストラクターは時間と空間内に実装されており、何も行いません。もちろん、クラスオブジェクト内にヒープ領域が作成された領域がない場合は、システムが生成した領域を使用してください。ただし、ヒープ領域に空き領域がある場合は、デストラクター内で手動で解放する必要があります。そうしないと、メモリ リークが発生しやすくなります。!!次に、クラス オブジェクト内に他のクラス メンバー属性がある場合、オブジェクトが破棄されるときにクラス メンバー属性のデストラクターが自動的に呼び出されるため、クラス オブジェクトのデストラクターで管理する必要がありません。!!
4 番目、コピー コンストラクター
4.1 コンセプト
コピー コンストラクターは C++ の特殊なコンストラクターで、オブジェクトがコピーされるときに新しいオブジェクトを作成し、元のオブジェクトの値を新しいオブジェクトにコピーするために使用されます。その機能は、元のオブジェクトと同じ内容を持つ新しいオブジェクトを生成することですが、これらは独立しており、一方のオブジェクトの内容を変更しても、もう一方のオブジェクトには影響しません。
コピー コンストラクターを明示的に定義しない場合、コンパイラーはデフォルトのコピー コンストラクターを生成します。デフォルトのコピー コンストラクターは、メンバー変数の値を 1 つずつコピーし、クラス内のポインター メンバーの浅いコピーを作成します(つまり、ポインターが指すオブジェクトをコピーするのではなく、ポインターの値をコピーします)。クラス内にディープ コピーが必要なリソース (動的に割り当てられたメモリなど) がある場合は、コピー コンストラクターを定義してディープ コピーを完了する必要があります。そうしないと、デストラクター内で領域の一部が繰り返し解放され、エラーが発生します。
4.2 特徴
- コピー コンストラクターは、コンストラクターのオーバーロードされた形式です。
- コピー コンストラクターのパラメーターは 1 つだけで、クラス型オブジェクトへの参照である必要があります。値渡しメソッドが使用されると、無限の再帰呼び出しが発生するため、コンパイラーは直接エラーを報告します。
ClassName(const ClassName& other);
4.3 例
クラスがありMyClass
、パラメータを値で受け取る不正なコピー コンストラクタを定義しようとしているとします。
上記の例では、 という
MyClass
クラスを定義し、値渡しを使用してコピー コンストラクターを定義しようとしました。obj2
コピー コンストラクターを使用してオブジェクトを作成しようとすると、無限再帰呼び出しが行われ、スタック オーバーフローが発生します。これは、値渡しメソッドがコピー コンストラクター自体を呼び出して、渡されたパラメーターのコピーを作成し、コピー コンストラクターの呼び出し中にパラメーターのコピーを再度作成するため、無限再帰が発生するために発生します。
無限の再帰呼び出しを回避するには、コピー コンストラクターのパラメーターを参照によって受け取る必要があります。これにより、コピー コンストラクターが呼び出されたときにオブジェクトの参照のみが渡され、新しいコピーは作成されません。
以下は、参照メソッドを使用して正しいコピー コンストラクターを定義した、修正されたサンプル コードです。
#include <iostream>
class MyClass {
public:
// 正确的拷贝构造函数
MyClass(const MyClass& obj) {
std::cout << "Copy constructor called." << std::endl;
}
};
int main() {
MyClass obj1;
MyClass obj2 = obj1; // 正确,使用引用方式传递参数
MyClass obj3(obj2); //此种方式也可以
return 0;
}
4.4 深いコピーと浅いコピー
浅いコピー (Shallow Copy) : 浅いコピーとは、オブジェクトをコピーするときに、ポインター メンバー変数の値を含む、オブジェクト内のメンバー変数の値のみがコピーされることを意味します。これは、新しいオブジェクト用に個別のリソース コピーを作成するのではなく、新しいオブジェクトと元のオブジェクトが同じリソースを共有することを意味します。元のオブジェクトにヒープ メモリを指すポインタ メンバーが含まれている場合、浅いコピーの後、新しいオブジェクトと元のオブジェクトのポインタ メンバーは同じヒープ メモリを指すことになり、その結果、2 つのオブジェクトが同じリソースを管理することになり、リソース解放の問題が発生する可能性があります。潜在的なエラー。
ディープ コピー (ディープ コピー) : ディープ コピーとは、オブジェクトがコピーされるときに、共有リソースではなく、新しいオブジェクトに対して独立したリソース コピーが作成されることを意味します。元のオブジェクトにヒープ メモリを指すポインタ メンバーがある場合、ディープ コピーは新しいオブジェクトのポインタ メンバーにメモリを個別に割り当て、元のオブジェクト ポインタが指す内容を新しいメモリにコピーします。このように、2 つのオブジェクトには独自の独立したリソースがあり、一方のオブジェクトのリソースを変更しても、もう一方のオブジェクトには影響しません。
#include <iostream>
#include <cstring>
#include <cstdlib>
class Person {
public:
// 构造函数
Person(const char* name, int age) {
_name = (char*)malloc(strlen(name) + 1);
strcpy(_name, name);
_age = age;
}
// 拷贝构造函数(浅拷贝)
Person(const Person& other) {
_name = other._name; // 浅拷贝,共享资源
_age = other._age;
}
// 深拷贝构造函数(深拷贝)
Person(const Person& other) {
_name = (char*)malloc(strlen(other._name) + 1); // 深拷贝,为新对象分配独立资源
strcpy(_name, other._name);
_age = other._age;
}
// 析构函数
~Person() {
free(_name);
}
// 打印个人信息
void PrintInfo() {
std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
}
private:
char* _name;
int _age;
};
int main() {
// 创建一个Person对象
Person p1("John", 30);
// 浅拷贝
Person p2(p1);
p1.PrintInfo(); // Output: Name: John, Age: 30
p2.PrintInfo(); // Output: Name: John, Age: 30
// 修改p1的值
p1 = Person("Alice", 25);
p1.PrintInfo(); // Output: Name: Alice, Age: 25
p2.PrintInfo(); // Output: Name: Alice, Age: 30(由于浅拷贝,p2共享p1的资源,也被修改为Alice)
// 深拷贝
Person p3(p1);
p1.PrintInfo(); // Output: Name: Alice, Age: 25
p3.PrintInfo(); // Output: Name: Alice, Age: 25(由于深拷贝,p3拥有独立的资源,不受p1的修改影响)
return 0;
}
5. 代入演算子のオーバーロード
5.1 コンセプト
代入演算子のオーバーロードは、C++ のカスタム クラスのメンバー間の代入演算を可能にする特別な関数です。代入演算子をオーバーロードすることにより、クラス オブジェクト間のカスタム代入動作を実装して、オブジェクトの正しいコピーとリソース管理を保証できます。
5.2 構文
返回类型 operator=(const 类名& 另一个对象) {
// 赋值操作的实现
// 返回对象本身的引用
}
このうち、戻り値の型は通常、連続的な代入操作をサポートできる参照型です。引数は、const
渡された代入演算子の右側のオブジェクトを表す参照です。
5.3 例
#include <iostream>
#include <cstring>
class Person {
public:
Person(const char* name, int age) {
_name = new char[strlen(name) + 1];
strcpy(_name, name);
_age = age;
}
// 拷贝构造函数
Person(const Person& other) {
_name = new char[strlen(other._name) + 1];
strcpy(_name, other._name);
_age = other._age;
}
// 赋值运算符重载
Person& operator=(const Person& other) {
if (this == &other) { // 自我赋值检测
return *this;
}
delete[] _name; // 释放旧资源
_name = new char[strlen(other._name) + 1];
strcpy(_name, other._name);
_age = other._age;
return *this; // 返回对象本身的引用
}
~Person() {
delete[] _name;
}
void PrintInfo() {
std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
}
private:
char* _name;
int _age;
};
int main() {
Person p1("John", 30);
Person p2("Alice", 25);
p1.PrintInfo(); // Output: Name: John, Age: 30
p2.PrintInfo(); // Output: Name: Alice, Age: 25
p2 = p1; // 赋值操作
p1.PrintInfo(); // Output: Name: John, Age: 30
p2.PrintInfo(); // Output: Name: John, Age: 30(p2被赋值为p1的内容)
return 0;
}
この例では、Person
クラス内の代入演算子をオーバーロードしました。オーバーロードされた関数では、まず自己代入が発生したかどうか (オブジェクト自体がそれ自体に割り当てられているかどうか) を確認し、発生した場合はオブジェクトへの参照を直接返します。次に、古いリソースを解放し (古い_name
メモリを削除し)、メモリを再割り当てして、新しいコンテンツをコピーします。