はじめに: C 言語の構造には、C++ の上位レベルの代替物 (クラス) があります。クラスのインスタンス化はオブジェクトと呼ばれます。
この記事は随時更新され、拡張されます。
1. プロセス指向とオブジェクト指向についての予備理解
C言語を勉強していた頃、プロセス指向とオブジェクト指向という言葉をよく聞きましたが、その理解は非常に曖昧でした。
C 言語はプロセス指向であり、プロセスに焦点を当て、問題を解決するための手順を分析し、関数呼び出しを通じて問題を徐々に解決します。
C++ はオブジェクト指向に基づいており、オブジェクトに焦点を当てており、1 つのものをさまざまなオブジェクトに分割し、オブジェクト間の相互作用に依存します。プロセスがどのように完了するかに注意を払う必要はなく、オブジェクト間の相互作用にのみ注意を払う必要があります。
オブジェクト指向には、カプセル化、継承、ポリモーフィズムという 3 つの大きな特徴があります。
2. カテゴリー
1. C++ の構造
C言語では構造体に変数しか定義できませんが、C++では構造体に変数だけでなく関数も定義できます(構造体はクラスにアップグレードされます)。
データ構造スタックを例として取り上げます。
関数を構造内で直接定義します。
オブジェクトをインスタンス化する場合、構造体を記述する必要はなく、構造体名のみを記述します。
#include<iostream>
using namespace std;
typedef struct Stack {
int* a;
int capacity;
int top;
void Init() //定义函数
{
a = nullptr;
capacity = 0;
top = 0;
}
}ST;
int main()
{
Stack s1; // 无struct
s1.Init();
return 0;
}
2. クラス定義
C++ では、構造体よりクラスが優先されます。
両者のデフォルトのアクセス制限にはいくつかの違いがあり、struct のデフォルトは public ですが、class のデフォルトは private であり、これはオブジェクト指向の要件により一致しています。。これが、私がクラスを使用することを好む理由です。この点については、以下のデフォルトのアクセス修飾子でも説明します。
class Classname
{
//类体:成员函数+成员变量
}; //跟结构体一样有分号不要忘
class はクラスを定義するキーワード、Classname はクラス名、{} はクラスの本体、クラス本体のコンテンツはクラスのメンバーと呼ばれ、クラス内の変数はクラスの属性またはクラス内のメンバ変数、関数が呼び出されますクラスメソッドまたはメンバー関数。
クラスを定義する 2 つの方法
1 つは、上記のスタックのようにクラス内に関数の宣言と定義を記述する方法ですが、この種の関数はコンパイラによってインライン関数として扱われることに注意してください。
ヘッダファイルにクラス宣言を記述し、ソースファイルに関数を定義する方法もありますが、メンバ関数名の前にクラス名::(ドメインスコープ修飾子)を追加する必要があるので注意してください。一般に、2 番目のタイプがよく使用されます。
//obj.h
#include<iostream>
using namespace std;
typedef struct Stack {
int* a;
int capacity;
int top;
void Init();
}ST;
//test.cpp
#include"obj.h"
void Stack::Init() // 类名::
{
a = nullptr;
capacity = top = 0;
}
3. クラスアクセス修飾子とカプセル化
C++ でカプセル化を実装する方法は、クラスを使用してオブジェクトのプロパティ (メンバー変数) とメソッド (メンバー関数) を組み合わせてオブジェクトをより完全なものにし、アクセス修飾子を通じてそのインターフェイスを外部ユーザーに選択的に提供することです。
アクセス修飾子には 3 種類あり、php、java などの言語にあります。
アクセス修飾子の説明
アクセス修飾子をうまく活用すると、クラス内のデータを効果的に保護し、他のユーザーが自由にデータにアクセスできないようにすることができます。
1.public: public クラスのメンバーにはどこからでもアクセスできます。
protected: 保護されたクラスのメンバーは、それ自体、そのサブクラス、および親クラスからアクセスできます。
private: プライベート クラス メンバーには、それが定義されているクラスからのみアクセスできます。
(継承を学ぶ前は、protect と private の使用に違いはありません。)
2. struct のデフォルトは public で、class のデフォルトは private です。
3. アクセス許可範囲は、アクセス修飾子から次のアクセス修飾子の出現までとなります。
4. その後にアクセス修飾子がない場合、スコープは } で終了します。
5. 通常の状況では、メンバー変数はプライベートに設定されます。
例として日付クラスを取り上げます。
class Date {
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year; //声明,没有定义,不占空间
int _month;
int _day;
};
4. クラスのインスタンス化
クラスからオブジェクトを作成するプロセスは、クラスのインスタンス化と呼ばれます。
1. クラスはオブジェクトを記述し、クラスのメンバーを制限するモデルのようなものです。クラスを定義しても、クラスを格納するための実際のメモリ領域は割り当てられません。
2. クラスは複数のオブジェクトをインスタンス化できます。インスタンス化されたオブジェクトのみが実際のメモリ空間を占有し、メンバー関数ではなくメンバー変数のみを格納します。
例として日付クラスを取り上げます。
class Date {
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year; //声明,没有定义,不占空间
int _month;
int _day;
};
int main()
{
Date d1; // 类的实例化
Date d2, d3; // 一个类可以实例化出多个对象
//下面两行代码可行吗,为什么?
//Date::_year = 1; //并没有实例化对象,只是声明没有开空间,更不必说初始化了。
//d1._year = 1; //实例化了呢?也不行,因为_year是私有成员变量,只能在Date类中更改。
return 0;
}
オブジェクトにはメンバー変数のみが格納され、メンバー関数は格納されません。
上で述べたように、クラスにはメンバー変数とメンバー関数という 2 つの本体があります。
しかし実際には、インスタンス化されたオブジェクトにはメンバー変数のみが格納され、メンバー関数はパブリック コード領域に格納されます。
次のコード例を見てください (クラスのスペース サイズの計算は構造体の計算と同じであり、構造体のメモリ アライメント ルールに従います)。
class Date {
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year; //声明,没有定义,不占空间
int _month;
int _day;
};
int main()
{
Date s1;
cout << sizeof(Date) << endl;
cout << sizeof(s1) << endl;
return 0;
}
コンソール出力は以下の通りです。
12はメンバ変数のみを計算した結果であることがわかり、オブジェクトにはメンバ関数が格納されていないことがわかります。
その理由は、各オブジェクトのメンバ関数は同じであり、パブリック コード領域に格納されるため、オブジェクトをインスタンス化するたびにメンバ関数を格納する必要がなくなり、プログラムの効率が大幅に向上します。
メンバー関数はパブリック コード領域に格納されます。
次のコードを見てください。正常に実行できると思いますか?
class Example
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
int _b;
};
int main()
{
Example* s1 = nullptr;
s1->Print(); //空指针指向????
return 0;
}
コンソールに次のように表示されます。
操作は成功しました。なぜですか? 上記は null ポインタ逆参照の問題ではありませんか? プログラムはクラッシュするのでしょうか?
回答: 上で述べたように、メンバー関数はパブリック コード領域に格納されます。関数のアドレスはパブリック コード領域で直接呼び出されます。オブジェクト s1 内で何かを探す必要はないため、null ポインタの逆参照操作は発生しない。
5.このポインタ
クラスのメンバー関数には this ポインター パラメーターが隠されています。
実パラメータや仮パラメータの位置に明示的に記述することはできませんが、クラス内で表示することは可能です。
このポインタは変更できません。
このポインタは null にすることもできます(これは、メンバー関数がパブリック コード領域に存在する上記の例です)。
このポインタはスタック フレームに格納されます。(これがオブジェクト内に存在すると誤解しないでください。これは仮パラメータであり、通常の仮パラメータと同様にスタック フレームに存在します)。
引き続き日付クラスを例として取り上げます。
class Date {
public:
//this在实参和形参中不能显示地写
//在类中可以显示地用(没什么价值)
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
/*void Init(Date* const this ,int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}*/
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2023, 8, 11); //d1.Init(&d1,2023,8,11);
return 0;
}
3. 6 つのデフォルトのメンバー関数
C++ にはデフォルトのメンバー関数が 6 つあり、これらを記述しなければ自動的に生成されます。
1.コンストラクター
コンストラクターの最も便利な点は、コンストラクターが自動的に呼び出されることです。これは、初期化を忘れたときに役割を果たすことができます。
コンストラクターは特別なメンバー関数です。コンストラクターの名前はコンストラクターと呼ばれていますが、コンストラクターの主なタスクはオブジェクトを作成するためのスペースを作成することではなく、オブジェクトを初期化する。
その特徴は次のとおりです。
1.関数名はクラス名と同じです。
2. 戻り値はありません(void を記述する必要はありません)。
3.コンパイラは、オブジェクトがインスタンス化されるときに、対応するコンストラクターを自動的に呼び出します。
4. コンストラクターはオーバーロードできます。(複数の初期化メソッドを提供するために複数のコンストラクターを作成できます)
class Date {
public:
//构造函数,函数名和类名相同。
Date(int year = 1, int month = 1, int day = 1) //全缺省参数
{
cout << "Date()" << endl; // 借此观察构造函数是否被调用
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,8,11); // 对象实例化时自动调用构造函数,一定记住!!! 实参可以任意更改
//在对象d1后面接实参是构造函数的特殊的初始化规则。
//Date d2(); //不可以在对象后加括号而不给实参,因为编译器分不清你是在创建对象还是调用函数。
return 0;
}
コンソール出力は次のとおりです。 ご覧のとおり、Date 関数は呼び出されず、オブジェクトがインスタンス化されたときに Date 関数が自動的に呼び出されています。
コンストラクターの機能
コンストラクターはデフォルトのメンバー関数の 1 つであり、これを記述しない場合はコンパイラーが自動的に生成します。
コンパイルによって生成されるデフォルトコンストラクターの特徴:
1.記述しても自動生成されず、記述しなければコンパイラがパラメータのないデフォルトのコンストラクタを自動生成します。
2 .組み込み型は処理されません(C++11は宣言時のデフォルト値をサポートしていますが、デフォルト値があれば処理されます)
3.カスタム型のメンバーのみが処理され、このメンバーのデフォルトのコンストラクターが呼び出されます。(これはコンストラクターではなく、デフォルトのコンストラクターであることに注意してください)(組み込み型は int や double などの言語によって提供される型であり、カスタム型は上記の Date など、自分で定義する型です。 なお、
int* は組み込み型であり、Date* は組み込み型でもあります。ポインタであれば組み込み型です)
デフォルトのコンストラクター
ps:初めて習った時はここがなかなか分かりづらく、混乱して混乱してしまいました。さらに学習し、より多くのコードを読む必要があります。
コンパイラーによって自動的に生成されたデフォルト コンストラクターだけがデフォルト コンストラクターであるとは決して考えないでください。パラメーターのないコンストラクターとすべてデフォルトのコンストラクター (どちらも自分たちで作成したもの) はデフォルト コンストラクターと呼ばれます。デフォルトのコンストラクターは 1 つだけです。
デフォルトのコンストラクターには 3 つのタイプがあります。
1. パラメーターのないコンストラクター
2. 完全なデフォルトのコンストラクター
3. コンパイラーによって自動的に生成されるコンストラクターは作成しませんでした。
概要: これら 3 つのデフォルト コンストラクターには、パラメーターを渡さずに呼び出すことができるという共通点があります。
複数のデフォルト コンストラクターが同時に存在する場合、あいまいさが生じます。
次の図に示すように、コンパイラはデフォルトのコンストラクターが存在しないことを表示します。
Date を all-default として書くと正常に実行できるようになります (上記の all-default コンストラクタに対応するコンストラクタがデフォルト コンストラクタです) ・要約: 通常の状況では、コンストラクタを自分で記述し、初期化方法を決定する必要があります
。すべてのメンバー変数がカスタム型である場合は、コンストラクターを作成しないことを検討できます。
初期化リスト (ハイパーリンク)
2. デストラクター
デストラクター: コンストラクターの役割とは異なり、デストラクターはオブジェクト自体の破棄を完了しません。ローカル オブジェクトの破棄はコンパイラーによって完了します。そしてオブジェクトが破棄されると、自動的にデストラクターが呼び出され、オブジェクト内のリソース (malloc、realloc スペースなど) のクリーンアップが完了します。
デストラクタの特徴:
1. デストラクタの名前はクラス名の前に文字を追加します ~
2. パラメータも戻り値もありません
3.クラスにはデストラクターを 1 つだけ含めることができます。定義が表示されない場合 (私たちが定義していない場合)、システムはデフォルトのデストラクターを自動的に生成します。注: デストラクターはオーバーロードできません。
4. オブジェクト宣言サイクルが終了すると、C++ コンパイル システムが自動的にデストラクターを呼び出します。
5. 後から定義したオブジェクトが先に破棄されます(スタックフレーム)。
デストラクターの特性
コンストラクターと同様に、デストラクターには次の特性があります。
1. 書いても自動生成されず、書かなければコンパイラが自動でデストラクタを生成します。
2. 組み込み型のメンバは処理されません。
3. カスタム型のメンバーは、このメンバーのデストラクターを呼び出します。
次のコードを例に挙げます。カスタム型 A のメンバー変数が日付クラスに作成されます。
class A {
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
{
cout << "Date()" << endl;
_year = year;
_month = month;
_day = day;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
A _aa;
};
int main()
{
Date d1;
return 0;
}
コンソールには次のように表示されます: Date のインスタンス化オブジェクトを作成したことがわかります。プログラムは最初に A のコンストラクターを呼び出し、次に Date のコンストラクターを呼び出しました。ライフ サイクルが終了した後、最初に Date のデストラクターを呼び出し、そして最後に A のデストラクタを呼び出しました。
A はカスタム型であり、カスタム型のメンバーはこのメンバーのコンストラクターとデストラクターを呼び出すためです。
3. コンストラクターのコピー
以下では、コピー構築が必要な理由を明らかにするために質問を使用します。
浅いコピーの問題 - 同じ空間を指す
上記の日付クラスを例として取り上げます。C 言語で一般的に使用される値渡し呼び出しを使用します。
//日期类代码跟上面相同,这里省略。 void Func(Date d) //传值调用 { } int main() { Date d1; Func(d1); return 0; }
デス
トラクターが 2 回呼び出されていることがわかります。これは、最初に d1 がメイン スタック フレームに作成され、コンストラクターが呼び出され、次に d1 がコピーされて d に割り当てられるためです。d のライフサイクルが終了すると、Func スタック フレームは破棄され、d のリソースをリサイクルするためにデストラクターが 1 回呼び出されます。その後、d1 のライフサイクルが終了し、スタック フレームが破棄され、d1 のリソースがリサイクルされ、2 番目のデストラクターが呼び出されます。
ただし、上記の date クラスには実際には、クリーンアップするために malloc と realloc によって開かれたスペースがありません。実際のコピー中にこの方法で値やパラメータを渡すことはできません。反例としてスタックを使用してみましょう:
class Stack {
public:
Stack(size_t n = 4)
{
_a = (int*)malloc(sizeof(int) * n);
_capacity = n;
_top = 0;
}
~Stack()
{
free(_a);
}
private:
int* _a;
int _top;
int _capacity;
};
void Func(Stack s)
{
}
int main()
{
Stack s1;
Func(s1);
return 0;
}
同様のコードですが、プログラムは実行直後にクラッシュします。
理由は上記の日付クラスの例と同様です。Func が最初にデストラクターを呼び出し、スタック フレームが破棄され、_a が指すスペースが解放され、メインの終了後、デストラクターは _ a が指す空間を解放するために呼び出されますが、この空間は解放されたばかりであり、 _a はすでにワイルド ポインターであるため、プログラムはクラッシュします。
それで、これに対する解決策はありますか?実際、パラメーターを値で渡すか、パラメーターを参照で渡すように Func を変更するだけで済みます。この場合、オブジェクト s1 は最初から最後まで 1 つだけ存在します (オブジェクトは 1 回だけ破棄されます)。
ただし、ここで別の疑問が生じます。s1 を変更せずに s を変更するにはどうすればよいでしょうか? この場合、コピー コンストラクターが使用されます。
コピー構築
現在の型のオブジェクトを使用して、同じ型の別のオブジェクトを初期化します。
C++ では、カスタム型の値によってパラメーターを渡すときにコピー構築を呼び出す必要があると規定しています。
コピー コンストラクター: 仮パラメータは 1 つだけです。この仮パラメータは、このクラス型のオブジェクトへの参照です。既存のクラス型オブジェクトを使用して新しいオブジェクトを作成するときに、コンパイラによって自動的に呼び出されます。
その特徴は次のとおりです。
1. コピー コンストラクターは、コンストラクターのオーバーロード形式です。
2. コピー コンストラクターのパラメーターは 1 つだけであり、クラス型オブジェクトへの参照である必要があります。値渡しメソッドを使用すると、無限の再帰呼び出しが発生するため、コンパイラーは直接エラーを報告します。
2 番目の点に注目してください。値渡し方式は使用できませんが、参照渡し方式は使用できます。
値渡しメソッドを使用する場合、値を渡されたパスがオブジェクト d をインスタンス化し、オブジェクト d が独自のコピー コンストラクターを呼び出して、オブジェクトをインスタンス化する...というように、無限の再帰呼び出しが続くためです。
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
{
cout << "Date()" << endl;
_year = year;
_month = month;
_day = day;
}
//Date(Date d) //拷贝构造函数——传值方式——程序崩溃
//{
// cout << "Date(Date d)" << endl;
// _year = d._year;
// _month = d._month;
// _day = d._day;
//}
Date(Date& d) //拷贝构造函数——传引用方式——正确
{
cout << "Date(Date d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1); // 拷贝构造
Date d3 = d1; //跟上一行代码是等价的
return 0;
}
ディープコピー
先ほど、Stackクラスの浅いコピーが発生し、同じ空間が2回解放されました。コピー コンストラクターを使用することでこれを回避できます。
パラメータを値で渡すと、オブジェクトがインスタンス化されます。コピーコンストラクターが自動的に呼び出されます、s1を変更せずにs1のデータをsにコピーします。
class Stack {
public:
Stack(int n = 4)
{
cout << "Stack()" << endl;
_a = (int*)malloc(sizeof(int) * n);
_capacity = n;
_top = 0;
}
Stack(Stack& s) // 拷贝构造函数
{
_a = (int*)malloc(sizeof(int) * s._capacity);
_top = s._top;
_capacity = s._capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
}
private:
int* _a;
int _top;
int _capacity;
};
void Func(Stack s) //自定义类型传值传参
{
}
int main()
{
Stack s1;
Func(s1);
return 0;
}
コピー構築の特徴:
私たちはそれを記述しません。デフォルトでコンパイラーによって生成されるコピー構造は、前のコンストラクターやデストラクターとは異なる特性を持っています。
1. カスタム型はコピー コンストラクター
2 を呼び出します。組み込み型、その値はコピーされます
概要: date クラスと同様に、コピー コンストラクターを記述する必要はなく、デフォルトで生成されたもので十分です。ただし、スタックなどの場合は、ディープ コピーのコピー構築を実装する必要があります。
4. 代入のオーバーロード
代入のオーバーロードを学習する前に、演算子のオーバーロードとは何かを理解する必要があります。演算子のオーバーロードにより、C++ クラスはより使いやすく、より魅力的になります。
組み込み型は演算子を直接使用できますが、カスタム型は演算子のオーバーロード自体を定義する必要があります。
例として、組み込み型 int および date クラスを取り上げます。
int s1,s2 = 10;
s1 = s2; //将s2的值赋给s1
Date d1(2023,8,12);
Date d2;
// d2 = d1???
カスタム型 d2 の値を d1 に割り当てたい場合は、代入演算子のオーバーロードを自分で定義できます。
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d) //拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& operator=(const Date& d) {
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Print() const
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 8, 12);
Date d2;
d2 = d1;
d1.Print();
d2.Print();
return 0;
}
コンソール出力は次のとおりです。
5. アドレス演算子とオーバーロード
6. const アドレス演算子とオーバーロード
アドレス演算子とオーバーロード、および const アドレス演算子とオーバーロードが一緒に導入されています。日常的に使用する場合、これら 2 つのデフォルトのメンバー関数は通常、私たちが記述する必要はありません。コンパイラによって自動的に生成されるもので十分なので、記述する必要はありませんもっと紹介してください。
& も演算子です。カスタム型の場合は、アドレス演算子のオーバーロードを自分で定義する必要があります。
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d) //拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date* operator&() // 取地址运算符重载
{
return this;
}
const Date* operator&() const //const取地址运算符重载,函数后的const修饰的是this指针
{
return this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
int day = 10;
Date d1(2023, 8, 12);
const Date d2(2023, 8, 12);
cout << &day << endl; //内置类型取地址
cout << &d1 << endl; // 取地址运算符重载
cout << &d2 << endl; //const取地址运算符重载
return 0;
}
記事末尾BB: ご不明な点がございましたら、お気軽にコメント欄にメッセージを残してください。記載内容に誤りがある場合は、コメント欄でご指摘ください。ブロガーが作成します。見たらすぐに修正。最後に、作成するのは簡単ではありませんが、友人に役立つ場合は、ブロガーに「いいね」や注目を与えたいと思います。