继承的概念
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。继承本质是类层次的复用。
继承的语法
class A {
public:
int a;
};
//以public的方式继承A
//派生类 继承方式 父类
class B : public A
{
public:
int _B;//此时B里面其实也包含了A
};
继承方式的不同让派生类继承的成员的访问方式有所变化:

知识点
1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的
3. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式
4. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
赋值兼容转换
派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去,但是父类不能赋值给子类
赋值兼容转换包括三种形式:
1. 派生类对象赋值给基类对象(发生对象切片)
2. 派生类地址赋值给基类指针(向上转型)
3. 派生类对象绑定到基类引用(向上转型)
class A {
public:
A(int input):_A(input) {
}
int _A;
};
class B :public A
{
public:
B(int input):A(3)
,_B(input) {
}
private:
int _B;
};
int main() {
B b(2);
A a(2);
a = b;
cout << a._A;//输出3,说明_A被b中的_A赋值
return 0;
}
继承中的名字隐藏机制
当派生类中声明了与基类同名的成员时,会触发C++的名字隐藏规则:
名字查找优先:编译器从当前类作用域开始查找,一旦找到匹配的名字就停止
声明即隐藏:只要派生类中有同名成员的声明(无论定义在哪里),基类的所有同名成员都会被隐藏
参数无关:隐藏不考虑参数匹配,只要是同名就会全部隐藏
作用域限定访问:被隐藏的基类成员仍然可以通过Base::显式访问
关键点:隐藏是编译期的名字查找行为,不是运行时的函数选择。只要派生类作用域中有这个名字的声明,编译器就不会继续向上查找基类的同名成员。
派生类的默认成员函数
构造函数
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用(有的话,编译器会自动调用基类的默认构造函数,此时如果没有默认构造函数会报错)
不能在构造函数的内部对基类成员进行初始化,需要使用初始化列表进行初始化(原因可以查到,这里简单理解为C++的规定)。顺带一提,初始化列表初始化的顺序是按照声明的顺序来的,而基类肯定在子类之前声明,所以不管放在初始化列表的第几行都是第一个被初始化的
拷贝构造函数
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化,假如显式调用基类的构造
函数也是可以的,因为编译器只是确保基类成员得到初始化,但是这样不符合拷贝构造本身定义
class A{
public:
int a
};
class B:public A{
//...
B(B& temp) :A(temp){//至少显式调用A的构造函数
//如果没有默认的又不调用就会报错
_B = temp._B;
}
}
赋值运算符重载函数
派生类的operator=必须要调用基类的operator=完成基类的复制,调用方式值得注意,如下:
显示调用基类的赋值运算符重载函数
class A{
public:
int a
};
class B:public A{
//...
B& operator=(B& temp){
A::operator=(temp)//发生切片操作,注意不要写成temp.a之类的,a不是A类型
//等效于:this->A::operator=(temp) 又因为A::operator=()是对A内的成员操作的且A内的成员已经通过继承转移到B,所以该语句生效
}
析构函数
由于基类的成员不可能涉及子类(因为是用子类去继承基类的),而子类中的操作可能涉及基类(那我故意调用子类中被释放的成员不也一样吗?可能还没学到把)所以一般先释放子类然后编译器自动调用基类的构造函数,不需要显式调用。
友元关系不能被继承
很好理解,基类的友元函数到了子类那边就用不了,除非在子类也使用friend关键字修饰
静态成员的继承
基类的静态成员就算被多个派生类继承了,从始至终都只有一个成员
class A {
public:
A() {
a++;
}
static int a;
};
int A::a = 0;
class B :public A {
//...;
public:
B():A() {
}
};
class C :public A {
//...;
public:
C() :A() {
}
};
int main() {
B b;
C c;
cout << A::a;//输出2,因为调用了两次A的构造函数,且始终是一个a在进行++操作
return 0;
}
多继承
一个类可以继承多个类,需要分别指定每个基类的继承方式
class A {
//...
};
class B :public A {
//...
};
class C :public A,private B {
//...
};
菱形继承的问题
这是C++语法设计的一个缺陷之一,由多继承导致:假如A被B,C继承了,那B,C分别有A的成员,然后D继承了B和C,那D就会继承到两次A的成员,此时出现了二义性问题,这种继承的关系看起来就像菱形一样

解决方式
在D中单独开一块空间来存放B和C中重复的A的成员即可,实现的机制其实挺复杂的,采用的是虚继承和虚基表来解决该问题
虚继承:
class A{
//...
public:
int a;
};
// 虚继承关键字 通常在菱形的腰部使用
class B:virtual public A{
//...
};
class C:virtual public A{
//...
};
class D:public B,public C{
//...
//此时就不会出现“两个 a”了
};
虚基表
虚基表是当一个类选择虚继承另一个类的时在内存中产生的一个“表”,是用来存储偏移量的,比如变量a相对于变量b偏移了多少。
当D继承B,C的时候,前面说过会在D中单独搞一块空间来存放A中的成员,假如在D中的B,C需要对A的成员进行操作,它们是不知道这些成员放在哪的。D自己是知道的,D中的在B,C之外可以直接对A的成员进行操作
当B,C,D分别继承到A的时候,都会产生一个特定的指针来指向这个虚基表,从而根据虚基表的偏移量来确定A中成员的位置
其实单独的B,C继承A是没必要搞虚继承的,只是为了防止菱形继承的情况
虚基表的个数通常只有一个,虚基表中偏移量的个数是根据继承的基类的个数来确定的

除了虚基表以外,在B,C中直接为A的每个变量指针也可以解决这个问题(早期的C++设计),那后来为什么不用指针?
显然,假如A有很多个变量且B,C继承多个基类 ,指针的数量会很庞大,同时访问的效率也会大打折扣(每次都要解引用),所以搞出了虚基表
总结:
有了多继承,就存在菱形继承,有了菱 形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
继承和组合:
class A{
//...
};
class AA:public A{
//...//基类更改导致派生类也需要更改,依赖性高
};
class AAA:virtual public A{
//...
//如果是虚继承还有虚基表的额外开销
};
class A2{
//...
};
class B{
A a;//想用谁的就用谁的,有可能发生内联优化,效率更高
A2 a2;
AA aa;
};
继承对基类的依赖性高,而组合更加灵活,所以一般优先采用组合。当然继承并非没有价值,后面学习的多态则依赖于继承实现。继承和组合的根本区别不是"能不能用成员",而是"以什么身份、什么代价、什么灵活性去用"
继承和组合总结:
语义不同:继承是"我是",组合是"我用"
耦合度不同:继承是白盒(知道内部),组合是黑盒(只知接口)
灵活性不同:继承关系编译时固定,组合可以运行时改变
演化能力不同:继承链难改,组合部件易换
测试难度不同:继承难测,组合易测(可mock)
1175

被折叠的 条评论
为什么被折叠?



