C++中的继承以及菱形继承

继承:面向对象程序设计使代码可以复用的最重要手段,它允许程序员在保持原有类特性的基础上进行拓展,增加功能。

 

继承格式:class 派生类:(public,protected,private)基类

 

继承方式 

基类的public成员

基类的protected成员

基类的private成员

继承引起的访问控制关系变化概括

public继承

仍为public成员

仍为protected成员

不可见

基类的非私有成员在子类的访问属性都不变

protected继承

变为protected成员

仍为protected成员

不可见

基类的非私有成员在子类的访问属性都变为protected成员

private继承

变为private成员

变为private承欢

不可见

基类的非私有成员都变为子类的私有成员

一个关于继承的简单例子

class Base

{

public:

        Base()

        {

               _pri = 1;

               _pro = 2;

               _pub = 3;

               cout << "Base()" << endl;

        }

        ~Base()

        {

               cout << "~Base()" << endl;

        }

        void ShowBase()

        {

               cout << "_pri = " << _pri << endl;

               cout << "_pro = " << _pro << endl;

               cout << "_pub = " << _pub << endl;

        }

private:

        int _pri;

protected:

        int _pro;

public:

        int _pub;

};

class Derived:public Base

{

public:

        Derived()

        {

               _pro *= 10;

               _pub *= 10;

               _dpri = 4;

               _dpro = 5;

               _dpub = 6;

               cout << "Derived()" << endl;

        }

        ~Derived()

        {

               cout << "~Derived()" << endl;

        }

        void ShowDerived()

        {

               /*cout << "_pri = " << _pri << endl;

                _pri是Base类的私有成员,派生类不能够访问到*/

 

               cout << "_pro = " << _pro << endl;

               cout << "_pub = " << _pub << endl;

               cout << "_dpri = " << _dpri << endl;

               cout << "_dpro = " << _dpro << endl;

               cout << "_dpub = " << _dpub << endl;

        }

private:

        int _dpri;

protected:

        int _dpro;

public:

        int _dpub;

};

int main()

{

        Base base;

        base.ShowBase();

        Derived der;

        der.ShowDerived();     

        return 0;

}

 

测试结果如下

Base()                                 //调用Base类的构造函数

_pri = 1                              //下面三行输出Base类中的值

_pro = 2                             

_pub = 3

Base()                                 //构造派生类对象先调用基类的构造函数              

Derived()                            //再调用派生类自己的构造函数

_pro = 20                            //在派生类中可以访问到基类的protected成员变量

_pub = 30                           //在派生类中可以访问到基类的public成员变量

_dpri = 4

_dpro = 5

_dpub = 6

~Derived()                          //析构派生类对象的时候,先调用派生类自己的析构函数          

~Base()                               //再调用基类的析构函数

          

~Base()                               //析构基类调用基类的构造函数

 

关于继承的一些总结

  • 父类的private成员是不能够被子类继承的,因为对于子类来说父类的private成员是不可见的,那么如果一个类不想让别的类访问到自己的某些成员,但又想让子类访问到,那么就定义成protected,protected成员就是为了继承而出现,能够让这个访问类型的成员只能够被该类的子类访问到

  • public继承相当于是一个接口继承,保持is-a原则(一个子类就是(is)一个(a)父类对象),每个父类可用的成员对子类也可用

  • protected/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是has-a的关系原则(一个子类并不是一个父类对象,父类的公有成员或保护成员在子类只是保护的或是私有,只能说一个子类中有(has)一个(a)父类对象),一般来说不是特殊情况不会用到这两个继承,基本上的用的都是public继承

  • 不管是哪种继承方式,子类都可以访问到父类的公有成员和保护成员,父类的私有成员存在但是在子类中是不可见的,即不能够被子类访问到

  • 使用关键字class时默认的继承方式是private,使用关键字struct时默认的继承方式是public(不过最好显示的写出来,比如可以有这种写法class B:A,但是A默认继承方式是私有的,所以最好显示写出来)

 

 

 

继承与转换——赋值兼容规则(public继承,对应上面的is-a的具体体现,若是保护或私有继承就不能赋值

class A

{

public:

        int _a;

};

class B:public A

{

public:

        int _b;

};

int main()

{

        A a;

        a._a = 10;

        B b;

        b._a = 20;

 

        //切片:子类对象赋值给父类的对象/指针/引用

        a = b; //成功

        //b = (B)a;     //编译器不让有这种方式的强转

 

        A* p1 = &b; //成功

        A& r1 = b;  //成功

 

        B* p2 = (B*)&a; //强转成功,父类的指针强转成了子类的

        cout << p2->_b; //能够读到_b,是个随机值

        //p2->_b = 20;  

        //但是这里通过这个指针访问_b就越界了,_b能够被读到,是个随机值,但是不属于这个指针所管辖的地址,一旦尝试写这个_b,就会发生越界访问错误所以不被提倡这样用(是一个运行时错误,编译时发现不了)

        B& r2 = (B&)a;   

        //r2._b = 20;   //这里和上面一样,也会越界

        return 0;

}

  • 子类对象可以直接赋值给父类对象(切片/切割)

  • 父类对象不能赋值给子类对象

  • 父类的指针/引用可以指向子类对象(A* p1=&b  A& r1=b;)

  • 子类的指针/引用不可以指向父类对象(可以通过强转实现,但是容易发生越界的问题,不提倡使用

  • 切片:子类对象赋值给父类的对象/指针/引用(为多态打基础)

       C++中的继承以及菱形继承

 

 

继承体系中的作用域

  • 在继承体系中基类和派生类都有独立的作用域

  • 子类和父类中有同名成员,将访问子类中的,要访问父类的需要加上父类的作用域——这种现象叫做隐藏或是重定义

  • 重定义:子类重新定义和父类同名的成员(包括变量和函数),注意重定义是在不同的作用域中,而重载是在同一个作用域中的

  • 在实际中在继承体系里面最好不要定义同名的成员。

class A

{

public:

        int _a;

};

class B :public A

{

public:

        int _a;//与父类同名的成员

        int _b;

};

int main()

{

        B b;

        b._a = 1;//优先取子类自己作用域中的_a

        b.A::_a = 2;//访问A中的_a

        b._b = 2;

        return 0;

}

测试结果

C++中的继承以及菱形继承

 

继承体系中的静态成员

        如果一个基类定义了static成员,那么在整一个继承体系中,只有一个这样的成员,无论派生出多少子类,都只有一个

class A

{

public:

        static int _a;

};

int A::_a = 0;

class B:public A

{

protected:

        int _b;

};

int main()

{

        B b;

        cout << A::_a << " ";

        cout << B::_a << " ";

        B::_a = 5;

        cout << A::_a << " ";

        cout << B::_a << " ";

        return 0;

}

输出结果

C++中的继承以及菱形继承

这个结果说明,在一个继承体系中,这个静态成员有且只有一份

 

 

派生类的默认成员函数

在继承关系中,如果派生类没有显示定义类的六个默认成员函数(构造,拷贝构造,析构,赋值运算符重载,取地址运算符重载,const取地址运算符重载),编译系统会合成这六个默认的成员函数

 

所谓的合成默认成员函数:派生类只初始化自己的成员,父类部分调用父类的成员函数

 

继承关系中构造函数调用顺序

                   C++中的继承以及菱形继承

  • 基类没有缺省构造函数,派生类必须要在初始化列表中显式给出基类名和参数列表

  • 基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数

  • 基类定义了带有形参表构造函数,派生类就一定要定义构造函数

 

继承关系中析构函数调用顺序

           C++中的继承以及菱形继承

 

如果子类中没有显式定义构造函数

class Person

{

public:

        Person(const char * name)//这个构造函数不是缺省的

               : _name(name)

        {       

               cout << "Person()" << endl;

        }

protected:

        string _name; // 姓名

};

class Student : public Person

{

public://此处并没有显式的定义子类的构造函数

protected:

        int _id;

};

 

int main()

{

        Student s;//这里将会编译不通过,因为父类没有缺省的构造函数

        return 0;

}

如果把父类的构造函数设置为缺省的,就可以通过了。

class Person

{

public:

        Person(const char * name = "pigff")//这个构造函数是缺省的

               : _name(name)

        {       

               cout << "Person()" << endl;

        }

protected:

        string _name; // 姓名

};

class Student : public Person

{

public://此处并没有显式的定义子类的构造函数

protected:

        int _id;

};

 

int main()

{

        Student s;//此时编译通过

        return 0;

}

 

 

当我们把子类的构造函数显式定义出来,但是必须在初始化列表处显示给出基类名和参数列表

class Person

{

public:

        Person(const char * name = "pigff")//这个构造函数是缺省的

               : _name(name)

        {       

               cout << "Person()" << endl;

        }

 

        Person(const Person& p)

               :_name(p._name)

        {

               cout << "Person(const Person& p)" << endl;

        }

 

        Person& operator=(const Person& p)

        {

               cout << "Person operator=(const Person& p)" << endl;

               if (this != &p)

               {

                       _name = p._name;

               }

               return *this;

        }

 

        ~Person()

        {

               cout << "~Person()" << endl;

        }

protected:

        string _name; // 姓名

};

class Student : public Person

{

public:

        //合成构造函数    

        Student(const char* name, int id)//此时给出了子类显式定义的构造函数

               :Person(name)//在初始化类别处显式给出父类名和参数列表,会去调用父类的构造函数

               , _id(id)

        {

               cout << "Student()" << endl;

       }

          

         //合成拷贝构造函数  

        Student(const Student& s)

               :Person(s)//一样的,也要显示给出父类名和参数列表,调用父类的拷贝构造函数,这个地方会有切片

               ,_id(s._id)

        {

               cout << "Student(const Student& s)" << endl;

        }

 

        //合成赋值运算符重载

         Student& operator=(const Student& s)

         {

               cout << "Student operator=(const Student& s)" << endl;

               if (this != &s)

               {

                       Person::operator=(s);//显示调用基类的赋值运算符重载

                       _id = s._id;

               }

         }

 

         ~Student()

         {

                Person::~Person();

         }

      //这里析构函数和上面的函数不一样,不需要我们显式地去调用,父类的析构函数会在子类的析构函数完成后自动去调用

protected:

        int _id;

};

 

int main()

{

        Student s("pigff",5);//调用显式定义的构造函数并通过

        Student s1(s);//调用显示定义的拷贝构造函数并通过

        Student s2("Mike",6);

        s2 = s;//调用显示定义的赋值运算符重载函数并通过

        return 0;

}

 

关于析构函数不需要显式去调用的证明

C++中的继承以及菱形继承

这里输出了两句~Person(),一句是我们显式调用时输出的,另一句是在子类调用析构函数完成后自动调用了析构函数输出的。当我们把子类的析构函数中那句输出语句去掉的时候,如下

C++中的继承以及菱形继承

 

如何实现一个类不能够被继承?

  • 将构造函数设成是私有的(合成默认构造函数的时候,会去调用父类的构造函数,但是父类构造函数是私有的,压根就访问不到)

 

 

继承体系中的友元关系

        友元关系不能够被继承。即基类的友元能够访问基类的私有成员,但是不能访问子类的私有成员

class B;//在此处先声明一下

 

class A

{

        friend void Print(A& a, B& b);//友元函数Print

public:

        A(int a)

               :_a(a)

        {}

protected:

        int _a;

};

 

class B:public A

{

public:

        B(int a,int b)

               :A::A(a)//用父类的构造函数来初始化父类的成员

               ,_b(b)

        {}

protected:

        int _b;

};

void Print(A& a, B& b)

{

        cout << a._a << " ";//因为是A类型的友元,可以访问到A的私有或保护成员

        cout << b._a << " ";

     //虽然不是B类型的友元,但是是B的父类A类的友元,所以可以访问到B类型中父类的私有或保护成员

        //cout << b._b << " ";//会出现编译错误,基类的友元访问不到子类的私有或是保护成员

}

int main()

{

        A a(1);

        B b(2, 3);

        Print(a,b);

        return 0;

}

输出结果

C++中的继承以及菱形继承

 

关于继承的模型

 

单继承:一个子类只有一个直接父类

C++中的继承以及菱形继承

多继承:一个子类有两个或两个以上的直接父类

C++中的继承以及菱形继承

 

菱形继承

C++中的继承以及菱形继承

菱形继承容易导致二义性数据冗余的问题

C++中的继承以及菱形继承

现在一个D中有两个_a,一个是从B类中继承下来的,另一个是从C类中继承下来的,那么在使用的时候我们就不知道到底是哪一个_a了,所以这样就容易产生二义性的问题。我们可以在_a前面加上一个类名,比如B::_a=1,C::_a=2,那么这样就消除了二义性的问题。但是依然没有解决数据冗余的问题!!

class A

{

public:

        int _a;//这里声明成public方便后面访问

};

class B:public A

{

public:

        int _b;

};

class C:public A

{

public:

        int _c;

};

class D:public B, public C

{

public:

        int _d;

};

int main()

{

        D d;

        d.B::_a = 1;//通过指定是哪个类中的_a来达到消除二义性的目的

        d.C::_a = 2;

        cout << d.B::_a << " " << d.C::_a << endl;

        return 0;

}

输出结果如下

C++中的继承以及菱形继承

 

至于数据冗余,上面的方式消除不了,于是就引进了虚继承的概念

改一下上面的代码,引入关键字virtual

class A

{

public:

        int _a;//这里声明成public方便后面访问

};

class B:virtual public A

{

public:

        int _b;

};

class C:virtual public A

{

public:

        int _c;

};

class D:public B, public C

{

public:

        int _d;

};

int main()

{

        D d;

        d._a = 1;//这个时候直接访问就可以了,d这个对象里面只有一个_a了,解决了数据冗余问题

        return 0;

}

 

那么虚继承到底是怎么消除数据冗余的呢?

 

我们先来求一下没有实现虚继承时候的D对象模型的大小

cout << sizeof(d) << endl;

结果是20,这个很好理解,可以看之前的D对象的对象模型,总共有5个int类型的变量,就是20。

 

再来看一下实现虚继承时候的D对象模型的大小。在此之前我们先想一下,之前没有实现虚继承的时候有两个重复的_a,那么虚继承消除了数据冗余的情况下,意味着只有一个_a了,那么答案会是16吗

C++中的继承以及菱形继承

出乎意料,这里大小是24。那么问题来了,24-16=8,这多出来的8个字节的东西放了什么呢?

 

要了解这个东西,先来看下面的测试代码,先测试不是虚继承的情况

D d;

d.B::_a = 0;

d.C::_a = 1;

d._b = 2;

d._c = 3;

d._d = 4;

我们通过内存来看下一下d对象的对象模型

C++中的继承以及菱形继承

先继承B还是先继承C,这是通过声明次序的不同而不同, 比如是class D:public C,public B,那么C对象就会在B对象的上面。从这个内存模型中我们可以看到,正好是5个int,大小刚好是20个字节。

 

我们再来测试虚继承的情况,因为只有一个_a,我们对上面的代码稍作改动

D d;

d._a = 1;

d._b = 2;

d._c = 3;

d._d = 4;

再来看看d对象的对象模型

C++中的继承以及菱形继承

虚继承通过上图的方式,存了在16个字节的基础上,在B类和C类中多用了8个字节的大小来存放B类和C类到A类中_a的偏移量,所以16+8=24,所以这个d对象的大小就是24个字节。

 

我们把虚继承中的这个有关偏移量的表叫做虚基表

 

注意:至于为什么不是将偏移量存放在指定地址,而是在指定地址的后4个字节,是因为这4个字节是为多态预留的(在多态中我们还会来分析这个模型)

 

一般不到万不得已不要使用菱形继承虚继承,因为菱形继承虚继承也带来了性能上的损耗(毕竟多开了地址来存放偏移量)。