c++:继承相关的要点热点,以及菱形继承的底层实现

来源:互联网 发布:泸州古蔺网络问政平台 编辑:程序博客网 时间:2024/06/06 08:02

一.3种继承关系下基类成员在派生类的访问关系变化:
这里写图片描述

这个关系表,我相信在大学里学过c++的朋友们来说,一定不陌生,期末考试前,老师会说:”啊~,这是重点啊,给我好好背。”
但是这个需要背吗?我认为根本不需要,这就是访问关系的变化,可以理解为访问权限的缩小,总的来说:
1.基类的私有成员在派生类不可访问,如果基类成员不想在类外被访问但要在派生类中访问,就定义为保护成员。(所以保护成员限定符是因为继承而出现)
2.不论什么继承方式,在派生类都可以访问基类的公有成员和保护成员,基类的私有成员存在但不可见。
3.公有继承是is-a原则,一个派生类就是一个基类。
私有保护继承是has-a原则,基类的部分成员并未完全成为了子类接口的一部分。

二:赋值兼容规则:前提是公有继承 is-a

class Person{public:    int _idcard;//身份证};class Student : public Person{public:    int _stuid;//学号};class Teacher : public Student{public:    int _teaid;//工号};

简单来说,Student继承了Person,Teacher继承了Student(某位研究生,既是学生,也是一些本科生的老师)
1.子类给父类

void test1(){    Teacher t;    t._idcard = 111;    t._stuid = 222;    t._teaid = 333;    Student s;    s = t;//把子类赋值给父类,可以,切片    Student* p = &t;//把子类的地址传给父类的指针,可以,切片    Student& r = t;//把子类传给父类的引用,可以,切片}

如图:
这里写图片描述

2.父类给子类:
t = s;
Teacher* p1 = (Teacher*)&s;
Teacher& r1 = (Teacher&)s;
这里写图片描述
总结来说:
1.子类对象可以赋值给父类对象,父类的指针/引用可以指向子类对象。
2.父类对象不能赋值给子类对象,子类的指针/引用不能指向父类对象(强转可以,但注意不要越界)。

三;隐藏:父类和子类可以定义同名成员,子类成员屏蔽了父类对成员的直接访问

class A{public:    int _x;};class B : public A{public:    int _x;//该成员与父类的成员名字一样};

这时候就构成了隐藏:

void test1(){    B b;    b._x = 10;//子类成员屏蔽了父类对成员的直接访问,所以修改的是子类的_x。}

这里写图片描述
若想改变父类的_a,需要:

    b.A::_x = 20;//这样就对父类的_a进行了访问

这里写图片描述
下面来看一道据说%90的人都会做错的题目:

class A{public:    void f()    {        cout << "A" << endl;    }public:    int _x;};class B : public A{public:    void f(int a)    {        cout << "B" << endl;    }public:    int _x;};void test1(){    B b;    b.f();}

改程序输出什么?
首先如果子类的成员函数如果不传参,那么好办,一定输出”B”,现在要传参,既然test里没有传,那就调用父类的喽,那就大错特错了。
答案是: error C2660: “B::f”: 函数不接受 0 个参数.
我要做出解释:
首先,隐藏对于成员函数,不看参数只管函数名!,所以它调用的还是子类的成员函数,那就会出错。
要想调用父类的成员函数,还要不出错,需要:

    b.f(10);    b.A::f();

这里写图片描述
所以一定要牢记概念。

四:派生类默认成员函数

class Person{public:    Person(const char* name = "")        :_name(name)    {        cout << "父类构造函数"<<endl;    }    Person(const Person& p)        :_name(p._name)    {        cout << "父类拷贝构造函数" << endl;    }    Person& operator=(const Person& p)    {        if (this != &p)        {            this->_name = p._name;            cout << "父类operator="<<endl;        }        return *this;    }    ~Person()    {        cout << "父类析构函数" << endl;    }protected:    string _name;};

首先,这是一个简单父类,写出了构造,析构,拷贝构造,operator=。
一个子类继承了它:
class Student : public Person;
要实现子类的默认成员函数,要知道,实现子类之前,要先将父类的实现:
构造:

    Student(const char* name = "",int id = 1)        :Person(name)//先实现父类的构造        , _id(id)//再初始化子类的    {        cout << "子类构造函数" << endl;    }

拷贝构造:

    //s2(s1)    Student(const Student& s)        :Person(s)//先拷贝父类的        ,_id(s._id)    {        cout << "子类拷贝构造函数" << endl;    }

operator=:

    //              s1 = s3;    Student& operator=(Student& s)    {        if (this != &s)        {            Person::operator=(s);//先赋值父类的,传this和s            this->_id = s._id;//再赋值子类的            cout << "子类operator=" << endl;        }        return *this;    }

析构函数:次函数较为特殊,虽然~Person()与~Student函数名不同,但是析构函数是特殊的函数,编译器编译时会都变为destucter,所以构成隐藏!并且析构函数满足后进先出,先析构子类,后析构父类。
程序验证:

//子类析构    ……    ~Student()    {        cout << "子类析构函数" << endl;    }};void test2(){    Student s;}

这里写图片描述
所以说,子类析构函数,不要调用父类的析构,若显示调,反而出错甚至崩溃。

五:写一个不能被继承的类。
拿到这个问题,想了一下很简单,直接把这个类的构造函数写成私有的,那么在其他类的无法实例出对象,就不能被继承。
可是这是一个杀敌一万自损八千的办法,因为这个类就不能实例出对象,那不是个花瓶吗?

class A{private:    A(int a = 10)        :_a(a)    {}private:    int _a;};class B : public A{public:    B(int b = 10)        :A(b)        , _b(b)    {    }private:    int _b;};//错误是:error C2248: “A::A”: 无法访问 private 成员(在“A”类中声明)

所以现在的问题是,如何既能不被继承,又能实例化出对象。
我有两个方法:
1.类里写一个静态函数(无this),传int型数据,返回A(a),返回一个对象,然后新建一个对象用返回值初始化,等于是调用了拷贝构造函数。

class A{public:    static A GetObj(int a)    {        return A(a);    }private:    A(int a = 10)        :_a(a)    {}private:    int _a;};void test(){    A a(A::GetObj(10));//拷贝构造}

2.在类里写一个静态函数(无this指针),参数是int型,返回类型是A*,返回一个new A(a),然后用一个A*类型的指针指向返回的地址。

……    static A* GetObj(int a)    {        return new A(a);    }…………    A* ptr = A::GetObj(10);

六:菱形继承及底层实现
首先明确一个概念,单继承和多继承:
这里写图片描述
所谓菱形继承,就是;
这里写图片描述

B继承了A,C继承了A,D继承了B和C,构成了一个像菱形模型。

代码实现;

class A{public:    int _a;};class B : public A{public:    int _b;};class C : public A{public:    int _c;};class D : public B, public C{public:    int _d;};

这样会出问题,什么问题呢,简单来说,就是二义性和数据冗余。
这里写图片描述
D d;
那么对象d里既有父类B的_a,又有父类C的_a,造成了数据冗余和二义性。

void test1(){    D d;    d._a = 1}

就会出错:error C2385: 对“_a”的访问不明确
想要访问明确的_a,还需:

    d.B::_a = 1;    d.C::_a = 2;

但有时候这种情况是不现实的,比如如果_a是身份证号,一个人就不可能有两个身份证号码。
为了解决二义性和数据冗余,采用了虚继承——virtual。
class B : virtual public A;
class C : virtual public A;
也就是将代码稍作修改:

class A{public:    int _a;};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;};

这样改变一个另一个也会变:
这里写图片描述
需要注意的是,virtual加的位置。
没有virtual时,sizoef(d) = 20;这很好理解,他一共有5个成员(_a,_b,_a,_c,_d),按照逻辑,加上virtual,那么sizeof(d)y应该是16,可以确是24,这是怎么回事呢?
我们应该从内存的角度看,因为监视有时候是化妆的,但内存是素颜。
首先,做一下赋值:

    D d;    d.B::_a = 1;    d.C::_a = 2;    d._b = 3;    d._c = 4;    d._d = 5;

取d的地址,得到;
这里写图片描述

那么我可以猜想,最上面两个是B,中间两个是C,最后是A,也就是

这里写图片描述
那么菱形虚继承的模型就是:
这里写图片描述
那么为什么是这样,B里的00 d7 df 80和C里的00 d7 d9 88又是什么?
再看内存2:
这里写图片描述
第一个是00 d7 df 80,第二个是00 d7 d9 88。里面有两个地址,其实是这样的:
这里写图片描述

经过计算,存的20,12就是偏移量,距离_a的距离。
分别是+20字节到A,+12字节到A。
虽然此例中字节数比不加virtual时更大了,但是如果sizeof(A)特别大时,则会节省空间,节省的字节数为:
sizeof(A)-8。
现在这些问题我相信你已经有了答案:
1.什么是菱形继承
2.菱形继承有什么问题
3.怎么解决这个问题
4.编译器是如何解决的