#include #include "complex.h"
名前空間 std を使用します。
ostream& 演算子 << (ostream& os, const complex& x) { return os << '(' << real (x) << ',' << imag (x) << ')' }
int main() { 複素数 c1(2, 1); 複素数 c2(4, 0);
cout << c1 << endl; cout << c2 << endl;
cout << c1+c2 << endl; cout << c1-c2 << endl; cout << c1 / 2 << endl;
cout << conj(c1) << endl; cout << Norm(c1) << endl;
cout << (c1 += c2) << endl;
cout << (c1 == c2) << endl; cout << (c1 != c2) << endl; cout << +c2 << endl;
cout << (c2 - 2) << endl; cout << (5 + c2) << endl;
0を返します。
上の図に示すように、次の理由により、「=」代入演算子をオーバーロードするときに自己代入をチェックする必要があります。
自己割り当て検出がない場合、自身のオブジェクトの m_data が解放され、m_data が指すコンテンツが存在しないため、コピーに問題が発生します。
説明のために 2 つのクラスを示します。
class complex
{
public:
complex (double r = 0, double i = 0)
: re (r), im (i)
{ }
complex& operator += (const complex&);
double real () const { return re; }
double imag () const { return im; }
private:
double re, im;
friend complex& __doapl (complex*,
const complex&);
};
class String
{
public:
String(const char* cstr = 0);
String(const String& str);
String& operator=(const String& str);
~String();
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
これら 2 つのオブジェクトを作成した後、コンパイラ (VC) は次のように 2 つのオブジェクトにメモリを割り当てます。
左側の 2 つは、デバッグ モードとリリース モードでのクラス複合体のコンパイラ メモリ割り当てです。デバッグ モードでは、コンパイラは先頭と末尾 (赤色の部分)、4*8 + 4 サイズの情報部分 (灰色の部分) を複合オブジェクトのメモリに挿入します。緑色の部分は、計算後に複合オブジェクトが実際に占有する領域です。セクションは 52 ワードしかありませんが、VC は 16 バイトにアラインされているため、52 の最も近い 16 の倍数は 64 であり、12 バイトのギャップを埋める必要があります (シアンのパッド部分)。リリース部の複合オブジェクトは情報ヘッダーとヘッダー部のみ追加されます。文字列クラスの解析も基本的には同じです。
次に、VC コンパイラーが配列オブジェクトをどのように割り当てるかを見てみましょう。
同様に、コンパイラーはオブジェクトに冗長な情報を追加します。配列には 3 つのオブジェクトがあるため、コンパイラーは 3 つの複合オブジェクトの前に「3」を挿入して、個々のオブジェクトの番号をマークします。 。 Stringクラスの解析方法も同様です。
以下の図は、配列オブジェクトの削除に delete[] メソッドが必要な理由を示しています。
文字列.h
#ifndef __MYSTRING__
#define __MYSTRING__
class String
{
public:
String(const char* cstr=0);
String(const String& str);
String& operator=(const String& str);
~String();
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
#include <cstring>
inline
String::String(const char* cstr)
{
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else {
m_data = new char[1];
*m_data = ' ';
}
}
inline
String::~String()
{
delete[] m_data;
}
inline
String& String::operator=(const String& str)
{
if (this == &str)
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
inline
String::String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
#include <iostream>
using namespace std;
ostream& operator<<(ostream& os, const String& str)
{
os << str.get_c_str();
return os;
}
#endif
string_test.cpp
#include "string.h"
#include <iostream>
using namespace std;
int main()
{
String s1("hello");
String s2("world");
String s3(s2);
cout << s3 << endl;
s3 = s1;
cout << s3 << endl;
cout << s2 << endl;
cout << s1 << endl;
}
C++ では、1 パラメーターのコンストラクター (または最初のパラメーターを除くすべてのパラメーターのデフォルト値を持つ複数パラメーターのコンストラクター) は 2 つの役割を果たします。 1 はコンストラクター、2 はデフォルトの暗黙的な型変換演算子です。
したがって、場合によっては、AAA = XXX のようなコードを作成し、XXX の型が AAA 単一パラメーター コンストラクターのパラメーター型である場合、コンパイラーは自動的にこのコンストラクターを呼び出して AAA オブジェクトを作成します。
これは見た目もかっこよくてとても便利です。 しかし、場合によっては (以下の信頼できる例を参照)、それは私たち (プログラマ) の本来の意図に反します。 現時点では、コンストラクターの前に明示的な変更を追加して、このコンストラクターが明示的にのみ呼び出し/使用でき、型変換演算子として暗黙的に使用できないことを指定する必要があります。
明示的なコンストラクターは、暗黙的な変換を防ぐために使用されます。以下のコードを見てください。
class Test1
{
public:
Test1(int n)
{
num=n;
}//普通构造函数
private:
int num;
};
class Test2
{
public:
explicit Test2(int n)
{
num=n;
}//explicit(显式)构造函数
private:
int num;
};
int main()
{
Test1 t1=12;//隐式调用其构造函数,成功
Test2 t2=12;//编译错误,不能隐式调用其构造函数
Test2 t2(12);//显式调用成功
return 0;
}
Test1 のコンストラクターは int パラメーターを受け取り、コードの 23 行目は暗黙的に Test1 のコンストラクターを呼び出すように変換されます。 Test2 のコンストラクターは明示的として宣言されています。これは、暗黙的な変換を通じてコンストラクターを呼び出すことができないことを意味するため、コードの 24 行目でコンパイル エラーが発生します。
通常のコンストラクターは暗黙的に呼び出すことができます。明示的なコンストラクターは明示的にのみ呼び出すことができます。
Fraction を double 型に変換する必要がある場合、変換のために double() 関数が自動的に呼び出されます。上の図に示すように、コンパイラーは double d = 4 + f の分析中に 4 が整数であると判断し、引き続き f を決定します。f が double() 関数を提供し、double を実行することがわかります。 f に対して () 演算を実行し、 0.6 を計算し、それを 4 に加算して、最終的に double 型の 4.6 を取得します。
上の図では、Fraction というクラスが定義されています。f+4 演算中に、「+」演算子がクラス内でオーバーロードされ、「4」がコンパイラによって暗黙的に Fraction オブジェクトに変換され (コンストラクター) 渡されます。 through Fraction オーバーロードされた「+」演算子が演算に参加します。
上図に示すように、Fraction に double() 関数が追加されます。これは、Fraction の 2 つのメンバー変数を分割し、f+4 のオーバーロード処理中にそれらを強制的に double 型に変換して結果を返します。エラーを報告するには、次の分析を行うことができます。
1. まず、4 が暗黙的に Fraction オブジェクトに変換 (コンストラクター) され、次にオーバーロードされた "+" 演算子が "f" で演算されて Fraction オブジェクトが返されます。
2. まず、4 が暗黙的に Fraction オブジェクトに変換 (コンストラクター) され、次にオーバーロードされた "+" 演算子と "f" 演算を通じてそのペアに対して double 演算が実行され、最後に Fraction オブジェクトが返されます。
3. 。 。
したがって、コンパイラには少なくとも 2 つの方法があり、曖昧さが生じてエラーが報告されます。 上図に示すように、コンストラクター Franction の前に明示的なキーワードを追加すると、暗黙的な変換がキャンセルされます。そのため、d2 = f + 4 の実行中に、f は double 関数を呼び出して 0.6 に変換し、その後に変換します。 4 に加算すると 4.6 になります。コンストラクターが暗黙的な数式変換をキャンセルするため、4.6 を分数に変換できないため、エラーが報告されます。
次の図は、C++ stl での演算子のオーバーロードと変換関数のアプリケーションを示しています。
次の図は、スマート ポインターの内部構造と使用法を示しています。 スマート ポインターの構文には 3 つの重要なポイントがあります。1 つ目は、上の図の T* px に対応する、保存された外部ポインターです。受信ポインターに関連する処理は、受信ポインターの代わりに実行されます。2 番目は、「*」演算子をオーバーロードし、ポインターが指すオブジェクトを返します。3 番目は、「->」演算子をオーバーロードします。 return 上の図に対応するポインタは px です。
イテレータもスマート ポインタの一種であり、上で説明したスマート ポインタの 3 つの要素もあります。これらは、以下の図の赤いフォントと黄色のマークの部分に対応します。
イテレータのオーバーロードの「*」と「->」のオーバーロード文字を以下で注意深く分析します。
リスト反復子オブジェクト list::iterator ite を作成します。ここでのリストは、リスト テンプレート定義内のクラスである Foo オブジェクトを保存するために使用されます。 T、operator*() は (*node).data オブジェクトを返します。node は __link_type 型ですが、__link_type は __list_node<T>* 型です。ここでの T は Foo なので、node は __list_node<Foo >* 型です。 *node).data は Foo 型のオブジェクトを取得し、&(operator*()) は最後に Foo オブジェクトのアドレスを取得します。つまり、Foo* を返します。型へのポインタ。
上の図からわかるように、各ファンクターは「()」演算子をオーバーロードしたクラスであり、実際にはクラスですが、関数のプロパティを持っているように見えます。以下の図に示すように、各ファンクターは実際にはその背後に奇妙なクラスを統合します。このクラスはプログラマが手動で明示的に宣言する必要はありません。 標準ライブラリのファンクターも奇妙なクラスを継承しています。このクラスの内容は次の図に示されています。このクラスにはいくつかのものが宣言されているだけで、実際の変数や関数はありません。具体的な内容については STL で説明します。
クラス テンプレートとは異なり、関数テンプレートを使用する場合、受信パラメーターの型を明示的に宣言する必要はありません。コンパイラーが自動的に型を推測します。
メンバー テンプレートは、一般的なプログラミングでよく使用されます。例として、T1 は U1 の基本クラスであり、T2 は U2 の基本クラスです。このようにして、渡された U1 と U2 の親クラスまたは祖先クラスが T1 と T2 である限り、この方法を通じて継承と多態性をうまく利用できますが、その逆はできません。このメソッドは STL でよく使用されます。
名前が示すように、テンプレートの部分化はテンプレート内で特定のデータ型を指定することを指します。これは一般化とは異なります。 もちろん、テンプレートの部分化にも次数があり、部分的な型を指定することもできます。これは部分特殊化と呼ばれます。
C++11講座では説明する内容が多すぎるので、とりあえずここで紹介するだけにしておきます。
C++11 コースでは説明する内容が多すぎるので、今回はここでのみ紹介します。
参照は、参照される変数のエイリアスとして見ることができます。
上の図に示すように、A、B、C の 3 つのクラスが定義されています。B は A を継承し、C は B を継承します。A には 2 つの仮想関数があり、B に 1 つ、C に 1 つあります。上図に示すように、コンパイラは A のオブジェクト a をメモリに割り当てます。メンバ変数は m_data1 と m_data2 の 2 つだけです。同時に、クラス A には仮想関数があるため、コンパイラはオブジェクト a に空間を割り当てます。仮想関数テーブルを保存するために、このテーブルにはこのクラスの仮想関数アドレス (動的関数) が保持されます。ステートフル バインディング)、クラス A には 2 つの仮想関数があるため、同様に、a の仮想関数テーブルには 2 つのスペース (黄色と青色のスペース) があり、それぞれ A::vfunc1() と A::vfunc2() を指します。クラス B はクラス A の vfunc1() 関数を書き換えるため、これはクラス B のオブジェクトです。したがって、B の仮想関数テーブル (シアンの部分) は B::vfunc1() を指し、B はクラス A の vfunc2() を継承するため、B の仮想関数テーブル (青色の部分) は親クラス A の A を指すことになります: :vfunc2 () 関数; 同様に、 c はクラス C のオブジェクトであり、次のように表されます。クラス C は親クラスの vfunc1() 関数を書き換えるため、C の仮想関数テーブル (黄色の部分) は C::vfunc1() を指すようになり、同時に C はスーパークラス A の vfunc2() を継承するため、B の仮想関数になります。テーブル (青い部分) は A::vfunc2() 関数を指します。同時に、上の図は C 言語コードを使用して、コンパイラーの最下層がこれらの関数を呼び出す方法を示しています。これがオブジェクト指向の継承ポリモーフィズムの本質です。
this ポインタは、実際には現在のオブジェクトのメモリ アドレスを指すポインタと考えることができます。上の図に示すように、基底クラスとサブクラスには仮想関数があるため、this->Serialize() は動的にバインドされます。これは (*(this->vptr)[n])(this) と同等です。前節の仮想ポインタと仮想関数テーブルを組み合わせると理解できますが、最終的になぜこのように書くのが正しいのかについては、次のまとめで説明します。