概述
面向对象编程技术非常看重软件的可重用性,在C++中,可重用性是通过继承机制来实现的。继承机制允许程序员在保持原有类的数据和功能的基础上进行扩展,增加新的数据和功能,从而构成一个新的类,也称为派生类。原有类,一般称之为基类。派生类不仅拥有基类的成员,还拥有自身新增加的成员。继承与派生是C++的重要组成部分,也是C++的基础知识。掌握好了继承与派生,就对面向对象编程技术有了更深刻的理解。关于继承与派生的入门知识,这里就不赘述了,下面将介绍继承与派生相关的一些知识要点。
访问权限
派生类从基类派生时,有三种继承方式,分别是:公有继承、保护继承、私有继承,分别对应关键字public、protected、private。
公有继承时,基类中public成员和protected成员在派生类中的访问权限不变,private成员在派生类中不可访问。
保护继承时,基类中public成员和protected成员在派生类中的访问权限变为protected,private成员在派生类中不可访问。
私有继承时,基类中public成员和protected成员在派生类中的访问权限都变为private,private成员在派生类中不可访问。
可通过下表更清晰地看到不同继承方式下,基类成员在派生类中的访问权限。
基类public成员 | 基类protected成员 | 基类private成员 | |
公有继承 | public | protected | 不可访问 |
保护继承 | protected | protected | 不可访问 |
私有继承 | private | private | 不可访问 |
派生类能否改变基类成员在自身的访问权限呢?答案是肯定的,通过using关键字即可。参看下面的示例代码。
class CBase{public: CBase(); int m_nData1;protected: int m_nData2;private: int m_nData3;};CBase::CBase() : m_nData1(6), m_nData2(88), m_nData3(999){ NULL;}class CDerived : public CBase{public: using CBase::m_nData2; protected: using CBase::m_nData1; using CBase::m_nData3; // 编译错误};CDerived derived;printf("data1 is %d\n", derived.m_nData1); // 编译错误printf("data2 is %d\n", derived.m_nData2); // 编译正常
可以看到,通过using 基类::基类成员的方式,可以修改基类成员在派生类中的访问权限。m_nData1原来为public,修改后变为protected。m_nData2原来为protected,修改后变为public。由于m_nData3在基类CBase中为private,故在派生类中无法访问,因此无法通过using CBase::m_nData3修改private成员的访问权限。
构造顺序
构造派生类的对象时,会按照顺序依次调用以下函数。
1、所有基类的构造函数。注意是根据继承基类的顺序,而不是派生类中初始化列表的顺序来调用的。
2、派生类所有成员变量的构造函数。注意是根据声明成员变量的顺序,而不是初始化列表的顺序来调用的。
3、派生类的构造函数。
在派生类的初始化列表中,如果没有显式调用基类和成员变量的构造函数,则自动调用基类和成员变量默认的构造函数。另外,销毁派生类对象时,调用析构函数的顺序与上面的顺序正好相反。
可以通过下面的示例代码更好地理解构造顺序和析构顺序。
class CTemp1{public: CTemp1() { printf("CTemp1 constructor\n"); } ~CTemp1() { printf("CTemp1 destructor\n"); }};class CTemp2{public: CTemp2(int nNumber) { printf("CTemp2 constructor: %d\n", nNumber); } ~CTemp2() { printf("CTemp2 destructor\n"); }};class CTemp3{public: CTemp3() { printf("CTemp3 constructor\n"); } ~CTemp3() { printf("CTemp3 destructor\n"); }};class CBase1{public: CBase1() { printf("CBase1 constructor\n"); } ~CBase1() { printf("CBase1 destructor\n"); }};class CBase2{public: CBase2(int nNumber) { printf("CBase2 constructor: %d\n", nNumber); } ~CBase2() { printf("CBase2 destructor\n"); }};class CBase3{public: CBase3() { printf("CBase3 constructor\n"); } ~CBase3() { printf("CBase3 destructor\n"); }};class CDerived : public CBase1, public CBase2, public CBase3{public: CDerived() : m_tmp2(66), m_tmp1(), CBase2(88), CBase1() { printf("CDerived constructor\n"); } ~CDerived() { printf("CDerived destructor\n"); }private: CTemp1 m_tmp1; CTemp2 m_tmp2; CTemp3 m_tmp3;};CDerived derived;
上述示例的输出如下:
CBase1 constructorCBase2 constructor: 88CBase3 constructorCTemp1 constructorCTemp2 constructor: 66CTemp3 constructorCDerived constructorCDerived destructorCTemp3 destructorCTemp2 destructorCTemp1 destructorCBase3 destructorCBase2 destructorCBase1 destructor
从输出可以得出以下几点。
1、在CDerived的初始化列表中,先初始化了m_tmp2和m_tmp1,但依然先调用了三个基类的构造函数。
2、在CDerived的初始化列表中,先初始化了CBase2,然后才初始化了CBase1,但依然按照继承基类的顺序先调用了CBase1的构造函数。
3、在CDerived的初始化列表中,没有初始化CBase3,但依然调用了CBase3的默认构造函数。如果CBase3没有默认构造函数,则编译出错。
4、在CDerived的初始化列表中,先初始化了m_tmp2,然后才初始化了m_tmp1,但依然按照声明成员变量的顺序先调用了m_tmp1的构造函数。
5、在CDerived的初始化列表中,没有初始化m_tmp3,但依然调用了m_tmp3的默认构造函数。如果m_tmp3没有默认构造函数,则编译出错。
6、析构的顺序与构造的顺序正好相反。
同名覆盖
派生类和基类可以拥有同名的成员,此时,派生类会把基类中所有同名的成员(包括多个重载版本的函数)都覆盖掉。要想调用基类的成员,必须加作用域。
class CBase{public: CBase() : m_nData(66) { NULL; } void ShowData() { printf("CBase data is %d\n", m_nData); } void ShowData(const std::string &strData) { printf("CBase data is %s\n", strData.c_str()); } int m_nData;};class CDerived : public CBase{public: CDerived() : CBase(), m_nData(88) { NULL; } void ShowData() { printf("CDerived data is %d, %d\n", m_nData, CBase::m_nData); } int m_nData;};CDerived derived;derived.ShowData();derived.ShowData("CSDN"); // 编译出错printf("data is %d\n", derived.m_nData);derived.CBase::ShowData();derived.CBase::ShowData("CSDN");printf("data is %d\n", derived.CBase::m_nData);CBase *pBase = &derived;pBase->ShowData();
上述示例的输出如下:
CDerived data is 88, 66data is 88CBase data is 66CBase data is CSDNdata is 66CBase data is 66
可以看到,调用派生类对象derived的成员函数ShowData()和成员变量m_nData时,都是使用的派生类中的成员。调用派生类对象derived的ShowData(“CSDN”)时,因为同名会覆盖基类中多个重载版本的函数,导致基类中重载字符串参数的函数在派生类中被隐藏了,不可见,故会报编译错误。加上作用域后,便可以指定访问基类中的成员变量和成员函数。另外,将基类指针指向一个派生类对象时,用基类指针调用的都是基类中的成员(不涉及虚函数,属于静态绑定)。
多继承
多继承是指派生类同时从多个基类派生。如果多个基类中有同名的public和protected成员,则会引起歧义,导致编译出错。由于基类中的private成员在派生类中不可访问,故同名的private成员不会引起问题。解决多基类同名的方法为:使用作用域访问指定基类中的同名成员。可参看下面的示例代码。
class CBase1{public: CBase1() : m_nNumber(66), m_strText("Hello"), m_bPassed(true) { NULL; } int m_nNumber;protected: std::string m_strText;private: bool m_bPassed;};class CBase2{public: CBase2() : m_nNumber(88), m_strText("CSDN"), m_bPassed(false) { NULL; } int m_nNumber;protected: std::string m_strText;private: bool m_bPassed;};class CDerived : public CBase1, public CBase2{public: void Show() { // 编译出错 printf("data is %d, %s\n", m_nNumber, m_strText.c_str()); // 输出:CBase1 data is 66, Hello printf("CBase1 data is %d, %s\n", CBase1::m_nNumber, CBase1::m_strText.c_str()); // 输出:CBase2 data is 88, CSDN printf("CBase2 data is %d, %s\n", CBase2::m_nNumber, CBase2::m_strText.c_str()); }};
可以看到,由于CBase1和CBase2中均有m_nNumber和m_strText,在CDerived中直接访问这两个成员,会引发编译错误。通过指定作用域CBase1::和CBase2::,可以在派生类中指定访问CBase1和CBase2的成员。
虚函数
虚函数在C++中主要是为了实现多态机制。所谓多态,也就是用基类的指针指向派生类的实例,然后通过基类的指针调用派生类实例的成员函数。可参看下面的示例代码。
class CBase{public: CBase() : m_nData1(66) { NULL; } virtual void Test1() { printf("CBase Test1\n"); } virtual void Test2() { printf("CBase Test2\n"); } virtual void Test3() { printf("CBase Test3\n"); }private: int m_nData1;};class CDerived : public CBase{public: CDerived() : CBase(), m_nData2(88) { NULL; } virtual void Test1() { printf("CDerived Test1\n"); } virtual void Test2() { printf("CDerived Test2\n"); }private: int m_nData2;};CBase *pBase = new CDerived();pBase->Test1(); // 输出:CDerived Test1pBase->Test2(); // 输出:CDerived Test2
可以看到,通过基类指针pBase调用虚函数Test1和Test2时,调用的是派生类中的函数。
那么,虚函数是如何实现的呢?
在C++中,具有虚函数的类都有一张虚函数表。这个表相当于一个一维数组,用于存储类中所有虚函数的地址。编译器必须保证对象实例最开始的位置存放指向虚函数表的指针,然后才能存放其他成员。可通过下面的示例代码来理解虚函数表的概念。
typedef void (*Test)();CBase base;Test pTest1 = (Test)*((size_t *)*(size_t *)(&base));pTest1();Test pTest2 = (Test)*((size_t *)*(size_t *)(&base) + 1);pTest2();int nData = (int)*((size_t *)(&base) + 1);printf("data is %d\n", nData);CDerived derived;pTest1 = (Test)*((size_t *)*(size_t *)(&derived));pTest1();pTest2 = (Test)*((size_t *)*(size_t *)(&derived) + 1);pTest2();Test pTest3 = (Test)*((size_t *)*(size_t *)(&derived) + 2);pTest3();int nData1 = (int)*((size_t *)(&derived) + 1);int nData2 = (int)*((size_t *)(&derived) + 2);printf("data is %d, %d\n", nData1, nData2);
上述示例的输出如下:
CBase Test1CBase Test2data is 66CDerived Test1CDerived Test2CBase Test3data is 66, 88
可以看到,我们完全通过指针的操作就访问了类实例中的虚函数和成员变量。在上面的代码中,(size_t *)(&base)为指向虚函数表的指针,*(size_t *)(&base)为虚函数表的地址,相当于一维数组的地址,(size_t *)*(size_t *)(&base)指向虚函数表中第一个虚函数的地址,*((size_t *)*(size_t *)(&base))则是第一个虚函数。其他虚函数和成员变量的指针转换与此类似,这里不再赘述。下图给出了类实例base和derived的内存布局结构,供大家参考。
上面讨论的都是单继承的情况,多继承时,原理类似,只是更复杂些,这里就不再深入介绍了。
虚继承
虚继承是为了解决菱形继承中,存在多份公共基类的拷贝,从而导致二义性的问题。在定义派生类时,如果在基类的名字前面加上virtual关键字,则构成虚继承。先通过下面的示例代码来看看菱形继承的问题。
class CBase{public: CBase() : m_nNumber(1) { printf("CBase default constructor\n"); } CBase(int nNumber) : m_nNumber(nNumber) { printf("CBase constructor: %d\n", nNumber); }protected: int m_nNumber;};class CDerived1 : public CBase{public: CDerived1() : CBase(66) { printf("CDerived1 constructor\n"); }};class CDerived2 : public CBase{public: CDerived2() : CBase(88) { printf("CDerived2 constructor\n"); }};class CFinal : public CDerived1, public CDerived2{public: CFinal() : CDerived1(), CDerived2() { printf("CFinal constructor\n"); } void Show() { printf("CFinal show: %d\n", m_nNumber); // 编译出错 printf("CFinal show 1: %d\n", CDerived1::m_nNumber); printf("CFinal show 2: %d\n", CDerived2::m_nNumber); }};CFinal final;final.Show();
上述示例的输出如下:
CBase constructor: 66CDerived1 constructorCBase constructor: 88CDerived2 constructorCFinal constructorCFinal show 1: 66CFinal show 2: 88
可以看到,CFinal继承了CDerived1和CDerived2,而CDerived1和CDerived2都继承了CBase。在CFinal中直接访问公共基类CBase中的成员变量m_nNumber时,便会存在二义性问题。虽然可以通过指定作用域来分别访问CDerived1和CDerived2中的m_nNumber,但CBase存在多份的问题仍然存在(调用了两次CBase的构造函数)。
再来看看使用虚继承时的示例代码。
class CBase{public: CBase() : m_nNumber(1) { printf("CBase default constructor\n"); } CBase(int nNumber) : m_nNumber(nNumber) { printf("CBase constructor: %d\n", nNumber); }protected: int m_nNumber;};class CDerived1 : public virtual CBase{public: CDerived1() : CBase(66) { printf("CDerived1 constructor\n"); }};class CDerived2 : public virtual CBase{public: CDerived2() : CBase(88) { printf("CDerived2 constructor\n"); }};class CFinal : public CDerived1, public CDerived2{public: CFinal() : CDerived1(), CDerived2() { printf("CFinal constructor\n"); } void Show() { printf("CFinal show: %d\n", m_nNumber); // 编译正常 printf("CFinal show 1: %d\n", CDerived1::m_nNumber); printf("CFinal show 2: %d\n", CDerived2::m_nNumber); }};CFinal final;final.Show();
上述示例的输出如下:
CBase default constructorCDerived1 constructorCDerived2 constructorCFinal constructorCFinal show: 1CFinal show 1: 1CFinal show 2: 1
可以看到,使用虚继承后,公共基类CBase的构造函数只调用了一次,且调用的是默认构造函数。这是因为虚基类的构造函数比较特殊,与常规的构造函数有所不同。由于从虚基类派生出来的每一个派生类中,其构造函数都调用了基类的构造函数,这样编译器就无法确定到底用哪一个派生类来构造基类对象。最终,编译器会忽略所有派生类中对基类构造函数的调用,而选择调用基类的默认构造函数。
如果基类的默认构造函数不存在,则编译器将报错。解决该问题的方法是:显式在CFinal中调用CBase的其他构造函数。示例代码如下:
class CFinal : public CDerived1, public CDerived2{public: CFinal() : CDerived1(), CDerived2(), CBase(2) { printf("CFinal constructor\n"); } void Show() { printf("CFinal show: %d\n", m_nNumber); printf("CFinal show 1: %d\n", CDerived1::m_nNumber); printf("CFinal show 2: %d\n", CDerived2::m_nNumber); }};