- 继承的概念及定义
- 继承关系和访问限定符
- 基类和派生类对象的赋值转换
- 继承中的作用域
- 派生类的默认成员函数
- 继承与友元
- 继承与静态变量
- 复杂的菱形继承以及虚拟继承
- 虚继承与菱形继承
- 继承的总结
- 继承和组合
继承是面向对象程序设计的重要特性,其本质就是类的复用。
这里属于 is - a 的关系,我们的student 和 teacher 都继承了父类(基类) person 。
继承后父类的person的成员 都会变为子类的一部分,实现类的复用。
继承关系和访问限定符
总结来说,继承后的访问限定是 基类成员 和 继承方式权限小的那一个。其中基类中的private是不可见的。
这里的不可见解释:
private也被继承到了派生类中,但是语法限制派生类对象不论在类里还是类外都无法访问它。
如果基类成员不想在类外被访问,但要被派生类访问,我们使用protected限定,我们可以看出,protected限定符是为了继承而出现的。
还有一个小细节是,class 默认继承方式是private ,而struct 是public ,我们一般不会省略继承方式。
实际运用中,一般都是使用public继承,通过控制基类的成员限定符来达到目的。
基类和派生类对象的赋值转换
派生类对象 可以给基类的对象、 基类的指针、基类的引用,进行赋值。也叫做赋值兼容规则。
从图中我们可以很轻易的看出我们的子类给父类赋值的时候,仅会将子类中父类的部分进行赋值,我们可以形象的理解为切片。这就是派生类给基类赋值。
代码中的第三点我们先挖一个坑,之后再来填。
我们要知道,基类和派生类属于独立的作用域,此时就会产生问题,如果基类和派生类中有同名成员,子类对象将会自动屏蔽掉父类继承下来的同名成员,我们称此情况为隐藏,也叫做重定义。当然我们可以使用访问限定符的方式来指定基类中的同名成员。
函数名如果相同 直接构成了 隐藏/重定义。在此我们可以对比一下函数重载,他们的区别是:是否在同一作用域。重载一定是在同一作用域下的函数名相同。
基类::同名成员 (显式访问)
我们有一个派生类对象s,很容易通过监视窗口看出我们继承的结构
派生类成员在下,基类在上,且同名成员有不同的值。
想分别访问这两个不同的num,我们可以通过访问限定符的方式,默认为当前作用域。
默认成员函数是指:我们不写编译器自动生成的函数。
其中有两种情况
1.我们真的没写
2.我们写了,但是是无参的或是全缺省的,我们都叫编译器自动生成
默认构造函数在类中只能有一个
接下来 ,我们来谈派生类中的默认构造函数
派生类的构造函数我们要构造两类东西,一个是自己的成员,还要构造父类的成员。
所以在构造自身的成员时,需要调用自己的构造函数,在构造父类的成员时,需要调用父类的成员函数。
class Person
{
public:
Person(const char* name)
: _name(name)
{
cout << "Person()" << endl;
}
}
class Student : public Person
{
public:
Student(const char* name, int id)
: Person(name)
,_id(id)
{
调用父类构造函数初始化继承的父类部分
再初始化自己的成员
cout << "Student()" << endl;
}
}
当然如果基类没有默认构造函数,如上代码情况,我们需要在派生类构造函数初始化列表中显式调用。
派生类对象初始化先调用基类构造在调用派生类构造。
拷贝构造也是同理,我们需要通过基类的拷贝构造完成基类的拷贝初始化,因为拷贝构造就是构造的一种重载。
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Student(const Student& s)
:Person(s) ->s传递给Person& s 是一个切片行为
, _id(s._id)
{
类似构造函数
cout << "Student(const Student& s)" << endl;
}
派生类的operator= 必须调用基类operator= 完成基类赋值
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
Student& operator=(const Student& s)/ {
// 同上
if (this != &s)
{
小心这里是隐藏
Person::operator=(s);
_id = s._id;
}
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
派生类的析构函数会在被调用完成后自动调用基类的析构函数,我们不需要向上面三个一样主动去调用基类的成员函数。
因为这样才能保证派生类对象先清理,基类对象后清理。
继承与友元
友元是什么,友元就是那个破坏封装的特性,我们都不怎么用。
友元函数一般定义在类外,然后在类内用friend 进行声明,这样,类外的函数就是这个类的友元。这个函数可以访问类内任意成员。
友元和继承有什么关系呢?
友元函数是不能继承的。父类有一个友元函数,继承下来。这个友元函数可不能任意对派生类私有和保护成员动手动脚。
这个Display可不能访问_stuNum这个派生类的保护成员。
基类定义了一个static静态变量,那么整个继承体系中,它具有唯一性。
无论派生了多少个子类,实例出的基类的static始终只有一个,这与static变量在内存中存放的位置有关,并不在栈帧中。
我们以上谈到的继承都是单继承,也就是单向一对一的继承方式。
那一定也会存在多继承,一个子类有两个或以上直接父类
至此还没有什么太大的问题,但就在这时,人们用了多继承发现,有一种特殊的多继承 —菱形继承,产生了很严重的问题。
菱形继承的问题:数据冗余和二义性。
也就是说在Assistant对象中会有两份Person成员
二义性:这两个同名变量_name,当我们要改变它的时候,究竟改变的是谁的数据,它的两个父类会受到牵连吗?这就叫做二义性。
c++开发人员处理了这个问题,但是数据冗余的问题无法解决。
此时c++实在无法容忍这样的数据冗余和二义性的效率问题,所以产生了一个新的语法:虚继承 virtual
虚继承与菱形继承虚继承体系:
我们先来看看一般的菱形继承究竟是怎么样的。
通过访问限定符指定B和C中的a,并对其赋值,其结构很好理解,我们再来看看虚继承的方式,其底层是怎么样的
首先我们发现,A已经不在B和C对象附近了,而是浓缩成了一份写入了D对象的最高位。
那么C类和C类中除了自己的成员变量 还有四个字节,我们可以发现这四个低字节就是我们的地址,是谁的地址呢?
我们通过内存窗口发现其存的为一个偏移量,这个偏移量指向了最高处的A。
也就是说,B通过这个地址找到虚基表,然后在虚基表中找到偏移量,经过计算得到它所继承下来的A。
当然虚基表中除了这个偏移量,还有四个字节是有关多态的,我们在此不详谈。
一般情况下,还是建议大家不要设计出菱形继承这样过于复杂的程序
继承的总结
C++语法复杂,其中一个点,就是多继承的存在,导致菱形继承,衍生出虚拟继承。导致底层的实现很复杂,我们使用、分析、调试起来,也很复杂。所以建议不要设计菱形继承这样的程序,在复杂度和性能上都不优秀。
你看隔壁java都没有多继承这种奇怪的东西。
public继承 ,它是一种is - a的关系,派生类都有一个基类
组合是一个种 has -a 的关系,B组合A,指每个B对象中都有一个A对象
我们优先使用对象组合而不是类继承
继承允许你根据基类的实现来定义派生类的实现,我们将这种生成派生类的复用称为白箱复用,因为基类的内部细节对子类是可见的。
对象组合则是通过组合对象来获得更加复杂的功能,我们称之为黑箱复用,因为组合对象的内部细节是不可见的。
这样就导致继承的耦合度高,组合的耦合度低,而我们程序设计就是要追求低的耦合度。
因为继承的共有和保护成员都可以被派生类调用,所以A的所有成员对B都是透明的,也就是说A的封装对于B来说效果是很低的。
而反观组合,只有被组合的对象的共有成员才会影响C类。所以这就意味着A对C的封装是相对良好的。



