自学内容网 自学内容网

C++继承(派生类的定义方式、派生类的隐藏、菱形继承、虚基表、基类成员地址)

C++继承(派生类的定义方式、派生类的隐藏、菱形继承、虚基表、基类成员地址)

1.继承的介绍

继承是面向对象程序设计的一个重要机制,通过继承,可以利用已有的数据类型来定义新的数据类型,实现代码复用。继承允许在保持原有类特性的基础上进行扩展,增加功能。

已存在的用来派生新类的类称为基类(或父类),由父类派生出的类称为派生类(或子类)。派生继承基类的成员,但派生类对基类成员的访问受继承方式和基类成员所处于的作用域限定符限制。

1. 派生类的定义方式

派生类的在定义时需要在名字后面加上: + 作用域限定符 + 父类名

class A
{
  //…………  
};
class B : public A
{
  //…………  
};

struct C
{
    //…………  
};

struct D : public C
{
  //…………  
};

2. 派生类的作用域限定符

  1. 派生类对基类的成员的访问权限是一个取小。也就是说继承方式的作用域限定符,和基类里的成员所处于的作用域限定符中取最小的权限由子类继承(public>protected>private)。

  2. 基类private的成员在派生类中无论以什么方式继承在派生类中都是不可访问的。如果即想让派生类访问某些成员,又不想在类外访问某些成员,就把这些成员放在protected里,protected限定符就是因为继承的需要才被发明的。

  3. class的默认继承方式是private,而struct的默认继承方式是public,不过做好显示写出继承方式。

  4. 在实际使用中一般都是public继承,很少使用protected和private继承。 因为protected和private继承的成员只能在派生类的类里使用,实际中拓展维护性不强。

3.派生类的切片赋值兼容

每一个派生类对象都可以看成是一个特殊的基类,也就是说派生类可以对基类赋值。

class A
{
  //…………  
};
class B : public A
{
  //…………  
};

int main()
{
    B b;
    A a = b;
    
    A& ret = b;
    A* ptr = &b;
}

基类和派生类中共有的成员变量会被赋值,而对于派生类中有但基类中没有的成员变量,编译器会将这些变量进行切割:

Inherit1

4. 派生类的隐藏

在继承体系中,基类派生类都有独立的作用域。基类和派生类中允许有同名的成员,派生类成员将屏蔽基类对同名成员的访问,也叫重定义。如果是成员函数,只要函数名相同就构成隐藏。在派生类中,如果想调用基类的同名成员需要使用基类::基类成员的方式来访问。事实上派生的隐藏应当尽量避免。

#include  <iostream>
using namespace std;
class A
{
public:
    void func()
    {
        cout << "func in A" << endl;

    }
};


class B :public A
{
public:
    int func()//只要函数名相同就构成隐藏
    {
        cout << "func in B" << endl;
        return 1;
    }
};
int  main()
{
    B b;
    b.func();
    b.A::func();
    return  0;
}

Inherit2

但要注意基类的静态成员属于当前类,也属于当前类的所有派生类,即静态成员只用一份,基类和派生类共用静态成员。

5. 派生类的默认成员函数

派生类的六大默认成员函数在使用时,涉及到对基类的成员的操作都需要特殊处理。

5.1 构造函数和拷贝构造函数

使用派生类的构造函数在初始化列表时,对于基类中的成员变量的初始化,需要写在基类的名字中:

class A
{
public:
    A(int x, int y)
        :_x(x),_y(y)
    {}
    int _x;
    char _y;
};

class B :public A
{

public:
    B(int x,char y,double z)
        :A(x,y),_z(z)
    {}
    double _z;
};

需要注意的是,在派生类中初始化列表基类的成员时,本质是调用了基类的构造函数,所以基类需要显示写一个构造函数。

对于拷贝构造,其方法也是相同的:

class A
{
public:
    int _x;
    char _y;
};

class B :public A
{

public:
    B(const B& b)
        :A(b),_z(b._z)
    {}
    double _z;
};

需要注意的是,A(b)中本质上是进行了切片赋值兼容。

5.2 析构函数

如果想在派生类的析构函数中调用基类的析构函数,需要显示写基类名::~::基类名()来调用。但实际上我们并不会去显示调用基类的析构函数,因为派生类的析构函数会先析构派生类的成员,然后自动调用基类的成员。如果显示去调用基类的析构函数,就会导致基类成员被析构两次,可能因此引发错误。

#include  <iostream>
using namespace std;
class A
{
public:
    ~A()
    {
        cout << "now ~A()" << endl;
    }
};


class B :public A
{

public:
    ~B()
    {
        A::~A();
        cout << "now ~B()" << endl;
     }
};

int  main()
{
    B b;
    return  0;
}

Inherit3

6.多基类造成的菱形继承问题

6.1 菱形继承

C++支持一个派生类有多个基类,但如果一个派生类的基类中,有两个以上的它的基类是同一个,就会造成菱形继承问题:

Inherit4

如图,D类发生了菱形继承

对于菱形继承,它会导致两个问题:

  1. 数据冗余:

    D类中同时继承了两次A类,那么D类中就会有两份A类的成员,造成了代码和资源的冗余。

  2. 二义性:

    D类中的两份A类成员可以使用B和C的作用域来分别赋值,这样就导致了一个D类中有两个同名的基类成员赋有不同的值,造成代码的混乱。

6.2 虚继承解决菱形继承

解决菱形继承引发的问题的方式是使用虚继承,使用方式是在继承时在继承方式前写一个 virtual ,如:

class B: virtual public A
{
    //……
};

需要注意的是,虚继承应当写在共有基类的派生类上,即 D 类发生了菱形继承,那么应当在 B 类和 C 类上虚继承 A 类。

使用虚继承的虚基类,在其派生类中实例化的对象,只存在唯一一份基类成员变量,即虚基类 B 和 C,在 D 类对象中,只存在一份 A 类的成员变量,从而避免了数据冗余和二义性。

虚继承能够解决菱形继承的原理是,编译器会为包含了 virtual 关键字的虚基类生成一个虚基表

6.3 虚基表的原理

以最简单的菱形继承为例。

Inherit4

首先,在 D 类实例化的对象中会继承 B 类和 C 类中的对象,其中包括一个虚基表指针(VBPtr)。D 类对象中会存在多个虚基表指针(具体个数要看菱形继承的情况),这些指针可能指向相同或不同的虚基表(Virtual Base Table, VBT)。其作用是通过虚基表中的偏移量信息唯一确定虚基类 A 的实例位置。

对于多个虚基表指针是否指向同一个虚基表的问题,虚基表要确保访问虚基类的成员时能正确解析偏移量,虚基表指针的内容可能在不同路径的继承中有所调整,如继承路径中引入了额外的层级,可能导致虚基表的偏移量不同,此时就会有不同的虚基表存在。

虚基表是一个静态数组( intlong 类型),存储了从派生类对象起始地址到虚基类成员的偏移量。即它的内部存储着 D 类对象的起始地址到 A 类非静态成员变量的偏移量。当我们通过 D 类获取到继承自 A 类的成员变量时,程序会通过 B 类(或 C 类)的虚基表指针找到虚基表,再通过虚基表对应下标的位置找到对应成员变量的相对偏移量,根据偏移量计算该成员的实际地址。这种机制避免了虚基类实例的重复,并正确解析了多路径继承中的成员访问问题。

Inherit5

#include  <iostream>
using namespace std;
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;
};

int  main()
{
    D d;
    cout << sizeof(d);
    return  0;
}

Inherit6

这里可以看到,D 类对象中所有的可见的成员变量为 4 个 int 类型 16 个字节,最终打印出来 D 类对象占 24 个字节,而多出来的 8 个字节在 32 位中正好是两个指针的大小。

在实践中可以设计多继承,但是切记要避免设计菱形继承,因为太复杂,容易出问题。

7. 继承的基类成员地址问题

单继承中,基类中的成员一般在派生类对象的首部。而对于多继承的情况,主要与代码中的继承顺序有关,先继承的基类会排在低地址,后继承的基类成员会排在高地址(类似于压栈),而对于虚继承的成员,就不一定能确定在什么地方,因为 C++ 没有对这个部分设立标准,实际存在在什么位置由编译器决定,在我测试的 VS 下,虚继承的部分在对象的最后。

#include  <iostream>
using namespace std;
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;
};

int  main()
{
    D d;
    cout << "D的地址: " << &d << endl;
    cout << "B的地址: " <<static_cast<C*>(&d) << endl;
    cout << "C的地址: " << static_cast<D*>(&d) << endl;
    cout << "A的地址: " << static_cast<A*>(&d) << endl;

    return  0;
}

Inherit7

Inherit8


原文地址:https://blog.csdn.net/2301_80163789/article/details/144166645

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!