目次
1. 単一継承: サブクラスに直接の親クラスが 1 つだけある場合、この継承関係は単一継承と呼ばれます。
2. 多重継承: サブクラスに 2 つ以上の直接の親クラスがある場合、この継承関係は多重継承と呼ばれます。
3. ダイヤモンド継承: ダイヤモンド継承は多重継承の特殊なケースです。
5. 以下は、上記の人物関係のダイヤモンド仮想継承の原理の説明です。
1. 継承の概念と定義
1. 継承の概念
継承メカニズムは、コードを再利用可能にするためのオブジェクト指向プログラミングの最も重要な手段であり、プログラマが元のクラスの特性を維持しながら機能を拡張および追加し、派生クラスと呼ばれる新しいクラスを生成できるようにします。継承はオブジェクト指向プログラミングの階層構造を表し、単純なものから複雑なものまでの認知プロセスを反映します。以前は、私たちが接してきた再利用は関数の再利用であり、継承はクラス設計レベルでの再利用でした。
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "popo"; // 姓名
int _age = 18; // 年龄
};
/*
继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。
这里体现出了Student和Teacher复用了Person的成员。
下面我们使用监视窗口查看Student和Teacher对象,可以看到变量的复用。
调用Print可以看到成员函数的复用。
*/
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
2. 継承の定義
2.1 フォーマットの定義
以下では、 Person が親クラスであり、基本クラスとも呼ばれます。Student はサブクラスであり、派生クラスとも呼ばれます。
2.2 継承関係とアクセス修飾子
継承方法:パブリック継承、プライベート継承、保護継承
アクセス修飾子: パブリック アクセス、プライベート継承アクセス、保護されたアクセス
2.3 継承された基本クラスのメンバーのアクセス方法の変更
クラスメンバー/継承メソッド | パブリック継承 | 保護された継承 | 私的継承 |
基本クラスのパブリックメンバー | 基本クラスのパブリックメンバー | 派生クラスの保護されたメンバー | 派生クラスのプライベート メンバー |
基本クラスの保護されたメンバー | 基本クラスの保護されたメンバー | 派生クラスの保護されたメンバー | 派生クラスのプライベート メンバー |
基本クラスのプライベートメンバー | 派生クラスでは表示されません | 派生クラスでは表示されません | 派生クラスでは表示されません |
要約:
- 基本クラスのプライベート メンバーは、継承方法に関係なく、派生クラスには表示されません。ここで非表示とは、基本クラスのプライベート メンバーが引き続き派生クラス オブジェクトに継承されますが、構文により、クラスの内部または外部にかかわらず、派生クラス オブジェクトによるアクセスが制限されていることを意味します。
- 基本クラスのプライベート メンバーには、派生クラスではアクセスできません。基本クラスのメンバーがクラスの外部から直接アクセスされることを望まないが、派生クラスではアクセスできるようにする必要がある場合、そのメンバーは保護されたものとして定義されます。継承により、保護されたメンバー修飾子が表示されていることがわかります。
- 実際、上記の表を要約すると、基本クラスのプライベート メンバーがサブクラスでは表示されないことがわかります。サブクラス内の基本クラスの他のメンバーのアクセス モード == Min (基本クラスのメンバーのアクセス修飾子、継承モード)、public > protected > private。
- キーワード class を使用する場合のデフォルトの継承メソッドは private で、struct を使用する場合のデフォルトの継承メソッドは public ですが、継承メソッドは明示的に記述することをお勧めします。
- 実際のアプリケーションでは、パブリック継承が一般的に使用され、プロテクト/プライベート継承はほとんど使用されません。また、プロテクト/プライベートから継承されたメンバーは派生クラスでのみ使用できるため、プロテクト/プライベート継承の使用も推奨されません。メンテナンスが強くない。
// 实例演示三种继承关系下基类成员的各类型成员访问关系的变化
class Person
{
public :
void Print ()
{
cout<<_name <<endl;
}
protected :
string _name ; // 姓名
private :
int _age ; // 年龄
};
//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected :
int _stunum ; // 学号
};
2. 基本クラスと派生クラスのオブジェクトの代入と変換
- 派生クラス オブジェクトは、基本クラス オブジェクト/基本クラス ポインター/基本クラス参照に割り当てることができます。ここにはスライスまたはカッティングと呼ばれる鮮やかな用語があります。派生クラスの親クラス部分を切り取って代入するということです。
- 基本クラスのオブジェクトを派生クラスのオブジェクトに割り当てることはできません。
- 基本クラスのポインターまたは参照は、キャストを通じて派生クラスのポインターまたは参照に割り当てることができます。ただし、基本クラス ポインターは、派生クラス オブジェクトを指している場合にのみ安全である必要があります。ここでの基本クラスがポリモーフィック型の場合、 RTTI (RunTime Type Information) のDynamic_cast を使用して識別し、安全な変換を実行できます。
class Person
{
protected :
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public :
int _No ; // 学号
};
void Test ()
{
Student sobj ;
// 1.子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj ;
Person* pp = &sobj;
Person& rp = sobj;
//2.基类对象不能赋值给派生类对象
sobj = pobj;
// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &sobj;
Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
ps1->_No = 10;
pp = &pobj;
Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
ps2->_No = 10;
}
3. 継承の範囲
- 継承システムでは、基底クラスと派生クラスは独立したスコープを持ちます。
- サブクラスと親クラスに同じ名前のメンバーが存在する場合、サブクラスのメンバーは親クラスが同じ名前のメンバーに直接アクセスできないようにブロックします。この状況は非表示と呼ばれ、再定義とも呼ばれます。(サブクラスのメンバー関数では、明示的なアクセスに基本クラス::基本クラスのメンバーを使用できます)
- メンバー関数の非表示の場合、非表示を構成するには関数名が同じである必要があるだけであることに注意してください。
- 実際には、継承システムで同じ名前のメンバーを定義しないことが最善であることに注意してください。
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected :
string _name = "popo"; // 姓名
int _num = 111; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout<<" 姓名:"<<_name<< endl;
cout<<" 身份证号:"<<Person::_num<< endl;
cout<<" 学号:"<<_num<<endl;
}
protected:
int _num = 999; // 学号
};
void Test()
{
Student s1;
s1.Print();
};
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" <<i<<endl;
}
};
void Test()
{
B b;
b.fun(10);
};
4. 派生クラスのデフォルトのメンバー関数
デフォルトのメンバー関数は 6 つあります。「デフォルト」とは、それらを記述しない場合、コンパイラが自動的に生成することを意味します。では、これらのメンバー関数は派生クラスでどのように生成されるのでしょうか?
- 派生クラスのコンストラクターは、基本クラスのコンストラクターを呼び出して、基本クラスのメンバーのその部分を初期化する必要があります。基本クラスにデフォルト コンストラクターがない場合は、派生クラス コンストラクターの初期化リスト フェーズ中に明示的に呼び出す必要があります。
- 派生クラスのコピー コンストラクターは、基本クラスのコピーの初期化を完了するために、基本クラスのコピー コンストラクターを呼び出す必要があります。
- 派生クラスのoperator=は、基本クラスのoperator=を呼び出して、基本クラスのコピーを完了する必要があります。
- 派生クラスのデストラクターは、呼び出された後に基本クラスのメンバーをクリーンアップするために、基本クラスのデストラクターを自動的に呼び出します。これにより、派生クラス オブジェクトが最初に派生クラス メンバーをクリーンアップし、次に基本クラス メンバーを順番にクリーンアップすることが保証されるためです。
- 派生クラス オブジェクトを初期化する場合、最初に基本クラスのコンストラクターが呼び出され、次に派生クラスのコンストラクターが呼び出されます。
- 派生クラス オブジェクトのデストラクターをクリーンアップするには、まず派生クラスのデストラクターを呼び出してから、基本クラスのデストラクターを呼び出します。
- 後続の一部のシナリオではデストラクターを書き換える必要があるため、書き換えの条件の 1 つは関数名が同じであることです。その後、コンパイラはデストラクタ名に特別な処理を実行して destructor() に変更するため、親クラスのデストラクタが virtual を追加しない場合、サブクラスのデストラクタと親クラスのデストラクタは隠された関係を形成します。
class Person
{
public :
Person(const char* name = "peter")
: _name(name )
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p )
{
cout << "Person operator = (const Person& p)" << endl;
if (this != &p)
_name = p ._name;
return *this ;
}
~Person()
{
cout << "~Person()" << endl;
}
protected :
string _name ; // 姓名
};
class Student : public Person
{
public :
Student(const char* name, int num)
: Person(name )
, _num(num )
{
cout << "Student()" << endl;
}
Student(const Student& s)
: Person(s)
, _num(s ._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator = (const Student& s )
{
cout << "Student& operator = (const Student& s)" << endl;
if (this != &s)
{
Person::operator =(s);
_num = s ._num;
}
return *this ;
}
~Student()
{
cout << "~Student()" << endl;
}
protected :
int _num ; //学号
};
void Test ()
{
Student s1 ("jack", 18);
Student s2 (s1);
Student s3 ("rose", 17);
s1 = s3;
}
5. 相続と友情
フレンド関係は継承できません。つまり、基本クラスのフレンドは、サブクラスのプライベートおよび保護されたメンバーにアクセスできません。
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
6. 継承と静的メンバー
基本クラスが静的メンバーを定義している場合、そのようなメンバーは継承システム全体で 1 つだけ存在します。サブクラスがいくつ派生しても、存在する静的メンバー インスタンスは 1 つだけです。
class Person
{
public :
Person () {++ _count ;}
protected :
string _name ; // 姓名
public :
static int _count; // 统计人的个数
};
int Person :: _count = 0;
class Student : public Person
{
protected :
int _stuNum ; // 学号
};
class Graduate : public Student
{
protected :
string _seminarCourse ; // 研究科目
};
void TestPerson()
{
Student s1 ;
Student s2 ;
Student s3 ;
Graduate s4 ;
cout <<" 人数 :"<< Person :: _count << endl;
Student ::_count = 0;
cout << " 人数 :" << Person ::_count << endl;
}
7. 複雑なダイヤモンド継承とダイヤモンド仮想継承
1. 単一継承: サブクラスに直接の親クラスが 1 つだけある場合、この継承関係は単一継承と呼ばれます。
2. 多重継承: サブクラスに 2 つ以上の直接の親クラスがある場合、この継承関係は多重継承と呼ばれます。
3. ダイヤモンド継承: ダイヤモンド継承は多重継承の特殊なケースです。
ダイヤモンド継承に関する問題: 以下のオブジェクト メンバー モデルの構築から、ダイヤモンド継承にはデータの冗長性とあいまいさの問題があることがわかります。Assistant オブジェクトには Person メンバーのコピーが 2 つあります。
class Person
{
public :
string _name ; // 姓名
};
class Student : public Person
{
protected :
int _num ; //学号
};
class Teacher : public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a ;
a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
仮想継承は、ダイヤモンド継承のあいまいさとデータの冗長性の問題を解決できます。上記の継承関係に示されているように、Student と Teacher が Person を継承するときに仮想継承を使用すると、問題を解決できます。仮想継承は他の場所では使用しないでください。
class Person
{
public :
string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
Assistant a ;
a._name = "peter";
}
4. データの冗長性と曖昧性を解決するための仮想継承の原理
仮想継承の原理を研究するために、単純化されたダイヤモンド継承システムを与え、メモリ ウィンドウを使用してオブジェクト メンバーのモデルを観察しました。
class A
{
public:
int _a;
};
// class B : public A class B : virtual public A { public: int _b; };
// class C : public A class C : virtual public A { public: int _c; };
class D : public B, public C { public: int _d; };
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
次の図は、ダイヤモンド継承のメモリ オブジェクト メンバー モデルです。ここでデータの冗長性がわかります。
次の図は、ダイヤモンド仮想継承のメモリ オブジェクト メンバー モデルです。ここでは、D オブジェクト内で、A がオブジェクト構成の最下位に配置されていることが分析できます。この A は B と C の両方に属しており、B と C はどのように配置されますか?共通の A を見つけてください? ここに、2 つのポインター B と C が指すテーブルがあります。この 2 つのポインタを仮想実表ポインタと呼び、これら 2 つのテーブルを仮想実表と呼びます。仮想ベーステーブルに格納されているオフセット。オフセットにより以下のものが見つかります。
/*为什么D中B和C部分要去找属于自己的A?
看看当下面的赋值发生时,d是不是要去找出B/C成员中的A才能赋值过去?*/
D d;
B b = d;
C c = d;
5. 以下は、上記の人物関係のダイヤモンド仮想継承の原理の説明です。
8. 継承に関するまとめと考察
- C++ の構文は複雑だとよく言われますが、実際には多重継承がその現れです。多重継承にはダイヤモンド継承があり、ダイヤモンド継承にはダイヤモンド仮想継承があり、基礎となる実装は非常に複雑です。したがって、通常、多重継承を設計することは推奨されません。また、ダイヤモンド継承を設計してはなりません。そうしないと、複雑さとパフォーマンスに問題が発生します。
- 多重継承は C++ の欠点の 1 つと考えられますが、Java などの最近の OO 言語の多くには多重継承がありません。
- 継承と構成:
- パブリック継承は is-a 関係です。つまり、すべての派生クラス オブジェクトは基本クラス オブジェクトです。
- 組み合わせは「有」と「一」の関係です。B が A を結合し、すべての B オブジェクトの中に A オブジェクトが存在すると仮定します。
- クラスの継承よりもオブジェクトの合成を優先します。
- 継承を使用すると、基本クラスの実装に基づいて派生クラスの実装を定義できます。派生クラスの生成によるこの種の再利用は、多くの場合、ホワイトボックス再利用と呼ばれます。「ホワイト ボックス」という用語は可視性を指します。継承では、基本クラスの内部詳細がサブクラスから見えます。継承により基底クラスのカプセル化がある程度破壊され、基底クラスの変更は派生クラスに大きな影響を与えます。派生クラスと基底クラス間の依存関係は非常に強く、結合度は高くなります。
- オブジェクトの合成は、再利用のためのクラス継承の代替手段です。オブジェクトを組み立てたり組み合わせたりすることで、より複雑な新しい機能を得ることができます。オブジェクトを合成するには、合成されるオブジェクトに明確に定義されたインターフェイスが必要です。このスタイルの再利用は、オブジェクトの内部の詳細が表示されないため、ブラックボックス再利用と呼ばれます。オブジェクトは「ブラック ボックス」としてのみ表示されます。結合されたクラス間には強い依存関係はなく、結合度は低いです。オブジェクトの構成を優先すると、各クラスをカプセル化した状態に保つことができます。
- 実際にできるだけ多くの組み合わせを試してみてください。この組み合わせは結合度が低く、コードの保守性が優れています。ただし、継承にも役割があり、一部のリレーションシップは継承に適しているため、継承を使用します。また、ポリモーフィズムを実現するには継承も必要です。クラス間の関係は継承したり組み合わせたりすることができますので、組み合わせてご利用ください。
// Car和BMW Car和Benz构成is-a的关系
class Car
{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
};
class BMW : public Car
{
public:
void Drive()
{
cout << "好开-操控" << endl;
}
};
class Benz : public Car
{
public:
void Drive()
{
cout << "好坐-舒适" << endl;
}
};
// Tire和Car构成has-a的关系
class Tire
{
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car
{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
Tire _t; // 轮胎
};