目录
1、对派生类中继承自基类的成员的访问方式的变化
总而言之,首先看基类的访问方式和派生类的继承方式,哪种方式更小那哪种就是基类成员在派生类中的访问方式,如果发现这么算下来的访问方式为private,那么还需要判断,如果这个private是因为在基类中就是private,那么在派生类中,对这个成员依然不可见,如果这个private是因为继承方式为private,那么在派生类中对该成员是可见的。
1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
2、继承与友元
友元关系不能被继承,比如说:B类为A类的友元,当C类继承B类时,C类和A类没有友元的关系。
3、基类和派生类对象赋值转换
1.派生类对象可以赋值给基类的对象/基类的指针/基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
2.基类对象不能赋值给派生类对象。
3.基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用dynamic_cast来进行识别后进行安全转换。(ps:这个我们后面c++11再讲解,这里先了解一下)
4、继承中的作用域
1.在继承体系中基类和派生类都有独立的作用域。
2.需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏,不需要看参数或者返回值,这里不会构成函数重载,因为函数的作用域不相同。
3.派生类和基类中有同名成员时,会隐藏基类成员,也叫重定义基类成员。在子类成员函数中,可以使用基类::基类成员访问被隐藏起来的基类成员。
4.注意在实际中在继承体系里面最好不要定义同名的成员。
5、继承体系中调用构造函数和析构函数的顺序
1.对于派生类来说,构造时是有顺序的,继承自基类的成员先构造,派生类的成员后构造。析构时也有顺序,派生类的成员先析构,继承自基类的成员后析构。即派生类对象初始化先调用基类构造再调派生类构造,派生类对象析构清理先调用派生类析构再调基类的析构。
6、派生类的默认成员函数
1.在派生类的构造函数中必须调用基类的构造函数初始化派生类继承自基类的那一部分成员,当基类没有默认构造函数时,必须在派生类构造函数的初始化列表中显示调用基类的构造函数。为什么不可以在派生类的构造函数的函数体中,也就是花括号中调用基类的构造函数初始化派生类对象中继承自基类的成员呢?因为调用构造函数的格式像在创建一个匿名对象,但实际上不是在创建匿名对象,假如基类的类名为A,则调用A类的构造函数的格式为A(),这是语法规定这么写的,之后编译器会为我们处理。如果在花括号里写A(),编译器就认为你在创建一个局部匿名的A类对象,而不是在初始化派生类对象中继承自基类的成员了。
初始化派生类对象中继承自基类的成员的错误格式如下
初始化子类对象中的父类成员的正确格式如下
当基类有默认构造函数(即不需要任何参数的构造函数)时,此时去初始化派生类中继承自基类的成员,可以不在派生类的初始化列表中显示调用基类的构造函数。
没有默认构造还不在派生类的初始化列表中显示调用基类的构造函数就会报错,如下图。
基类有默认构造,此时无需在派生类的构造函数中显示调用基类的构造函数。
上面两组结果对照可以得出一个结论:在派生类构造函数的初始化列表中有可能显示写基类的构造函数,也有可能不显示写基类的构造函数,但绝对不可以在派生类构造函数的函数体中写基类的构造函数。
问题:上面都是调用基类的构造函数初始化派生类对象中继承自基类的成员,那可不可以不调用构造函数,而是自己手动初始化一个个成员呢?
答案:不可以。如下图写法是错误的,可以理解为派生类初始化时是合成的,派生类的成员(这里的成员不包括从基类继承的成员)由派生类的构造函数初始化,如果在派生类构造函数的初始化列表里显示初始化基类成员,会报错,如下图中红色下划波浪线处报错。
2.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。派生类的拷贝构造函数需要独属于派生类的成员调用派生类的拷贝构造,继承自基类的成员调用基类的拷贝构造。如下图,派生类拷贝构造的写法和派生类构造函数的写法一样,只是参数变成了对派生类对象的引用。tips:Person中只有一个成员_name。
3.必须在派生类的operator=()中调用基类的operator=()完成对基类成员的复制。对于赋值运算符 operator=()来说,注意它也是函数,由于派生类和基类的函数名相同,所以派生类中隐藏了基类的赋值运算符,如果不突破类域访问基类的运算符,会造成无限递归自己的赋值运算符,最后造成栈溢出,所以编写派生类的赋值运算符的方法应该如下图。
4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。对于析构函数来说,这里和上面的其他默认成员函数有些不一样,虽然派生类和基类的析构函数名不相同,但这里派生类的析构和基类的析构会构成隐藏,原因是多态需要,派生类和基类的析构函数的名字都会统一成destructtor(),并且不需要在派生类的析构函数中手动调用基类的析构函数,而在派生类的其他默认成员函数中都是需要手动调用基类对应的成员函数的。
析构函数没有初始化列表,所以释放派生类对象中继承自基类的成员时只能在派生类的析构函数体中调用基类的析构函数。还有一个特殊的点就是不管你显不显示在派生类的析构函数体中写基类的析构,派生类析构结束时都会自动调用父类的析构,换言之如果在派生类显示写了基类的析构,那么基类的析构次数会是派生类的双倍次。
5.因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。基类与派生类的析构函数应该为虚函数(即加virtual关键字),如下图实验。
#include<iostream.h> classBase { public: virtual ~Base(){cout<<"~Base"<<endl;} }; classDerived:publicBase { public: virtual ~Derived(){cout<<"~Derived"<<endl;} }; void main(void) { Base*pB=newDerived;//upcast delete pB; } /// 输出结果为: ~Derived ~Base 如果析构函数不为虚,那么输出结果为: ~Base
6.基类的构造函数、析构函数、赋值函数都不能被派生类继承。
7、菱形继承
有ABCD四个类,假如A类成员只有一个整形变量_a,B类成员只有一个整形变量_b,其他类同理。假如B类和C类都继承A类,然后D类再继承B和C,此时D类成员中就有两个整形变量_a,假设D的实例叫x,那么正确访问两个_a的格式为x.B::_a和x.C::_a。错误格式为x._a,它会导致二义性,编译器不知道该访问哪个。这是菱形继承的第一个缺陷,第二个缺陷就是数据冗余,毕竟D类间接继承了两份A类的所有成员。
如何解决呢?通过虚拟继承。拿上面情景举例,只需要将B和C类继承A类时的继承方式都改成virtual继承即可。D类继承B和C时不用加virtual。
被虚拟继承的基类也被称为虚基类,只有当出现了菱形继承的情况时,才需要在继承列表中的类前面加virtual,虚基类也是可以被实例化的。
8、在继承体系中完全不需要用户编写的默认成员函数
对于其他默认成员函数,如取地址重载(&)一般是不用自己写的,因为就算是派生类,也不需要取父类对象的地址,只需要自己类对象的地址就行。