(1)不继承,意思是派生类中确实没有,不包含基类的构造和析构函数
(2)派生类自己有自己的构造和析构,规则和之前讲过的完全一样
(3)研究构造和析构函数时,一定要注意默认规则
(1)代码验证:在基类和派生类中都显式提供“默认构造”并添加打印信息,通过执行结果来验证
通过代码执行结果看到的现象总结:派生类的构造函数执行之前,会先调用基类的构造函数,然后再调用自己的构造函数。而在派生类的析构函数之后,会先执行自己的析构函数,再执行基类的析构函数。
(2)代码验证:派生类的任意构造函数,可以显式指定调用基类的任意一个构造函数,通过参数匹配的方式(类似于函数重载)
(1)牢记构造函数的2大作用:初始化成员,分配动态内存
(2)派生类和基类各自有各自的构造函数和析构函数,所以是各自管理各自的成员初始化,各自分配和释放各自所需的动态内存
(3)继承的语言特性,允许派生类调用基类的构造和析构函数,以管理派生类从基类继承而来的那些成员。
(4)明确:派生类的构造和析构处理的永远是派生类自己的对象,只是派生类对象模板中有一部分是从基类继承而来的而已。
(1)派生类构造函数可以直接全部写在派生类声明的class中,也可以只在clas中声明时只写派生类构造函数名和自己的参数列表,不写继承基类的构造函数名和参数列表,而在派生类的cpp文件中再写满整个继承列表,这就是语法要求
(2)派生类析构函数则不用显式调用,直接写即可直接调用基类析构函数。猜测是因为参数列表问题。
(3)构造函数的调用顺序是先基类再派生类,而析构函数是先派生类再基类,遵循栈规则。
(4)派生类的构造函数可以在调用基类构造函数同时,用逗号间隔同时调用初始化式来初始化派生类自己的成员
(1)吸收基类成员:除过构造和析构函数以外的所有成员全部吸收进入派生类中
(2)更改继承的成员。1是更改访问控制权限(根据继承类型还有成员在基类中的访问类型决定) 2是同名覆盖(派生类中同名成员覆盖掉基类中)
(3)添加派生类独有的成员。
(1)代码实验:派生类和基类中各自实现一个内容不同但函数原型完全相同的方法,会怎么样?
(2)结论:基类对象调用的是基类的方法,派生类对象调用执行的是派生类中重新提供的方法
(3)这种派生类中同名同参方法替代掉基类方法的现象,叫做:重定义(redefining),也有人叫做隐藏。
(4)隐藏特性生效时派生类中实际同时存在2份同名同参(但在不同类域名中)的方法,同时都存在,只是一个隐藏了 另一个
(1)派生类对象直接调用时,隐藏规则生效,直接调用的肯定是派生类中重新实现的那一个
(2)将派生类强制类型转换成基类的类型,再去调用则这时编译器认为是基类在调用,则调用的是基类那一个,隐藏规则被绕过了
(3)在派生类内部,使用父类::方法()的方式,可以强制绕过隐藏规则,调用父类实现的那一个
(1)其实不止成员方法,成员变量也遵循隐藏规则。
(2)隐藏规则本质上是大小作用域内同名变量的认领规则问题,实际上2个同名成员都存在当前派生类的对象内存中的
(3)隐藏(重定义,redefining),与重载(overload)、重写(override,又叫覆盖),这三个概念一定要区分清楚。
(1)C和C++都是强类型语言,任何变量和对象,指针,引用等都有类型,编译器根据类型来确定很多事
(2)派生类是基类的超集,基类有的派生类都有(但构造函数和析构函数例外),派生类有的基类不一定有,所以这2个类型间有关联
(3)派生类对象可以cast(类型转换相关的关键字)后当作基类对象,而基类对象不能放大成派生类对象,否则就可能会出错
(4)考虑到指针和引用与对象指向后,派生类和基类对象的访问规则就是所谓类型兼容规则。
(1)子类对象可以当作父类对象使用,也就是说子类对象可以无条件隐式类型转换为一个父类对象,但是将父类强转成子类是隐藏危险的。
(2)子类对象可以直接初始化或直接赋值给父类对象
(3)父类指针可以直接指向子类对象
(4)父类引用可以直接引用子类对象
(1)派生类对象可以作为基类的对象使用,但是只能使用从基类继承的成员。
(2)类型兼容规则是多态性的重要基础之一。
(3)总结:子类就是特殊的父类 (base *p = &child;)
(1)本质上为了代码复用
(2)继承方式很适合用来构建复杂框架体系
(3)用继承来设计类进而构建各层级对象,符合现实中的需要。举例:描述人的种群
(1)鸵鸟不是鸟问题。因为鸵鸟从鸟继承了fly方法但是鸵鸟不会飞
(2)圆不是椭圆问题。因为圆从椭圆继承了长短轴属性然而圆没有长短轴属性
(3)不良继承是天然的,是现实世界和编程的继承特性之间的不完美契合
(1)修改继承关系设计。既然圆继承椭圆是一种不良类设计就应该杜绝。去掉继承关系,两个类可以继承自同一个共同的父类,不过该类不能执行不对称的setSize计算,然后在圆和椭圆这2个子类中分别再设计以区分
(2)所有不良继承都可以归结为“圆不是椭圆”这一著名具有代表性的问题上。在不良继承中,基类总会有一些额外能力,而派生类却无法满足它。这些额外的能力通常表现为一个或多个成员函数提供的功能。要解决这一问题,要么使基类弱化,要么消除继承关系,需要根据具体情形来选择。
(1)composition,组合,就是在一个class内使用其他多个class的对象作为成员
(2)用class tree做案例讲解
(3)组合也是一种代码复用方法,本质也是结构体包含
class A
{
}
class B
{
}
class C
{
}
//用组合的方式实现
class D
{
A a;
B b;
C c;
short s1;
}
//用继承的方式实现
class C:public A, public B, public C
{
short s1;
}
(1)继承是a kind of(is a)关系,具有传递性,不具有对称性。
(2)组合是a part of(has a)的关系,
(3)继承是白盒复用。因为类继承允许我们根据自己的实现来覆盖重写父类的实现细节,父类的实现对于子类是可见的。
(4)继承的白盒复用特点,一定程度上破坏了类的封装特性,因为这会将父类的实现细节暴露给子类
(5)组合属于黑盒复用。被包含对象的内部细节对外是不可见的,所以它的封装性相对较好,实现上相互依赖比较小
(6)组合中被包含类会随着包含类创建而创建,消亡而消亡。组合属于黑盒复用,并且可以通过获取其它具有相同类型的对象引用或指针,在运行期间动态的定义组合(例如上边提到的子类对象可作为一个父类对象使用)。而缺点就是致使系统中的对象过多。
(7)OO设计原则是优先组合,而后继承
(1)多继承就是一个子类有多个父类
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};
(2)多继承演示
#include
using namespace std;
// 基类 Shape
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 基类 PaintCost
class PaintCost
{
public:
int getCost(int area)
{
return area * 70;
}
};
// 派生类
class Rectangle: public Shape, public PaintCost
{
public:
int getArea()
{
return (width * height);
}
};
int main(void)
{
Rectangle Rect;
int area;
Rect.setWidth(5);
Rect.setHeight(7);
area = Rect.getArea();
// 输出对象的面积
cout << "Total area: " << Rect.getArea() << endl;
// 输出总花费
cout << "Total paint cost: $" << Rect.getCost(area) << endl;
return 0;
}
(3)多继承和单继承的原理,效果并无明显区别
(4)多继承会导致二义性问题
(1)场景:C多继承自A和B,则C中调用A和B的同名成员时会有二义性
(2)原因:C从A和B各自继承了一个同名(不同namespace域)成员,所以用C的对象来调用时编译器无法确定我们想调用的是哪一个
(3)解决办法1:避免出现,让A和B的public成员命名不要重复冲突。但这个有时不可控。
(4)解决办法2:编码时明确指定要调用哪一个,用c.A::func()明确指定调用的是class A的func而不是class B的
(5)解决办法3:在C中重定义func,则调用时会调用C中的func,A和B中的都被隐藏了
(6)总结:能解决,但是都没有很好的解决。
(1)场景:菱形继承问题。即A为祖类,B1:A, B2:A, C:B1,B2,此时用C的对象调用A中的某个方法时会有二义性
class A{......};
class B1: public A{......};
class B2: public A{......};
class C: public B1, public B2{.....};
(2)分析:c.func()有二义性,c.A::func()也有二义性,但是c.B1::func()和c.B2::func()却没有二义性
(3)解决办法:和问题1中的一样,但是问题2更隐蔽,也更难以避免
(1)二义性就是歧义,好的情况表现为编译错误,不好的情况表现为运行时错误,最惨的情况表现为运行时莫名其妙
(2)随着系统的变大和变复杂,难免出现二义性,这不是程序员用不用心的问题,是系统自身带来的
(3)解决二义性问题不能靠程序员个人的细心和调试能力,而要靠机制,也就是编程语言的更高级语法特性
(4)虚函数、虚继承、纯虚函数、抽象类、override(重写,覆盖)、多态等概念就是干这些事的
(5)感慨:欲戴王冠必承其重,要揽瓷器活就得有金刚钻,C++学得越清楚就越能想象将来用C++去解决的都是些什么层次的问题
(1)场景:菱形继承导致二义性问题,本质上是在孙子类C中有B1和B2中包含的2份A对象,所以有了二义性。
(2)虚继承解决方案:让A和B虚继承D,C再正常多继承A和B即可,虚继承会使得C继承A、B时只选择AB其中的一份D,不会重复,类似于头文件不重复包含
#include
using namespace std;
//基类
class D
{
public:
D(){cout<<"D()"<<endl;}
~D(){cout<<"~D()"<<endl;}
protected:
int d;
};
class B:virtual public D
{
public:
B(){cout<<"B()"<<endl;}
~B(){cout<<"~B()"<<endl;}
protected:
int b;
};
class A:virtual public D
{
public:
A(){cout<<"A()"<<endl;}
~A(){cout<<"~A()"<<endl;}
protected:
int a;
};
class C:public B, public A
{
public:
C(){cout<<"C()"<<endl;}
~C(){cout<<"~C()"<<endl;}
protected:
int c;
};
int main()
{
cout << "Hello World!" << endl;
C c; //D, B, A ,C
cout<<sizeof(c)<<endl;
return 0;
}
(3)虚继承就这么简单,就是为了解决菱形继承的二义性问题而生,和虚函数(为了实现多态特性)并没有直接关系
(1)虚继承的原理是:虚基类表指针vbptr 和 虚基类表virtual table
vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
菱形继承中,两个虚基类表指针vbptr通过指针偏移量来找寻虚基类表(避免了二义性),但通过两个虚基类表最终找到的是同一个基类。
详解参考:https://blog.csdn.net/xiejingfa/article/details/48028491
注:本文章参考了《朱老师物联网大讲堂笔记》,并结合了自己的实际开发经历以及网上他人的技术文章,综合整理得到。如有侵权,联系删除!水平有限,欢迎各位在评论区交流。