您的位置:首页 > 编程语言 > C语言/C++

深入C++07:处理多继承的坑

2022-05-12 20:20 344 查看 https://www.cnblogs.com/d-book

📕处理多继承的坑

理解虚基类和虚继承

**虚基类: ** 被虚继承的类,就称为虚基类。

virtual作用: 1.virtual修饰了成员方法是虚函数。 2.可以修饰继承方式,是虚继承。被虚继承的类就称为虚基类。

vfptr:一个类有虚函数,这个类生成的对象就有vfptr,指向vftable。 vbptr:在派生类中从基类虚继承而来。 vftable:存放的RTTI指针(指向运行时RTTI信息)与虚函数地址。 vbtable:第一行为向上偏移量,第二行为虚基类指针,存离虚基类内存的偏移量。

例子:

class A
{
public:
private:
int ma;
};

class B : public A
{
public:
private:
int mb;
};
//A a; 4个字节
//B b; 8个字节

对象a占4个字节,对象8占8个字节。但如果我们给B的继承方式访问限定符前面加了一个virtual关键字。

我们使用命令:cl –d1reportSingleClassLayout[classname] xxx.cpp查看此时的内存布局(去实验一下)

此时:A类对象:内存不变化,为4字节; B类:不是之前的8个字节,变为12个字节,多了一个vbptr指针。原来最上面应该为ma与mb,但是现在多了一个vbptr(虚基类指针),ma跑到派生类最后面去了。vbptr指向的是vbtable,vbtable第一行为0,第二行为虚基类指针到虚基类数据的偏移量;

当我们遇到虚继承时候。要考虑派生类B的内存布局时,首先我们先不考虑虚继承。类B继承了基类的ma,还有自己的mb;当我们基类被虚继承后,基类变为虚基类,虚基类的数据一定要在派生类数据最后面,再在最上面添加一个vbptr。派生类的内存就由这几部分来构成。

虚基类指针(vbptr)指向虚基类表(vbtable),vbtable第一行为向上的偏移量,因为vbptr在该派生类内存起始部分,因此向上的偏移量为0;第二行为向下的偏移量(vbptr离虚基类数据的偏移量),原来基类的数据放到最后,找ma的时候还是在最开始找,但ma被移动,根据偏移的字节就可以找到

虚基类指针与虚函数指针一起出现

class A
{
public:
virtual void func()
{
cout << "call A::func" << endl;
}
private:
int ma;
};

class B : virtual public A
{
public:
void func()
{
cout << "call B::func()" << endl;
}
private:
int mb;
};

int main()
{
//基类指针指向派生类对象,永远指向的是派生类基类部分数据的起始地址!!!!!
A *p = new B();//B::vftable
p->func();
delete p;

return 0;
}

调用成功,但是delete会异常!!!

分析一下:

B的内存布局:B首先从A中获取vfptr与ma,B中还有自己的mb;此时A被虚继承,A中所有的东西都移动到派生类最后面,最上面补一个vbptr,vbptr指向vbtable,vfptr指向vftable;**我们基类指针指向派生类对象,永远指向的是派生类基类部分数据的起始地址。**普通情况下,派生类内存布局先是基类,在是派生类,基类指针指向派生类对象时,基类指针指向的就是派生类内存的起始部分。但是虚继承下,基类称为虚基类,虚基类的数据在派生类最后面,原地方补上vbptr,此时再用基类指针指向派生类对象时候,基类指针永远指向派生类基类部分的起始地址。虚基类一开始就是vfptr,这是能够用p指向的对象访问vfptr与vftable的原因。释放内存时候出错,因为对象开辟是在最上面即绿色部分,但是p所持有的是虚基类的地址,delete时从虚基类起始地址delete,因此出错。

重点看图里的1 -> 2 !

验证一下地址:

class A
{
public:
virtual void func()
{
cout << "call A::func" << endl;
}
void operator delete(void *ptr)
{
cout << "operator delete p:" << ptr << endl;
free(ptr);
}
private:
int ma;
};

class B : virtual public A
{
public:
void func()
{
cout << "call B::func()" << endl;
}

void* operator new(size_t size)
{
void *p = malloc(size);
cout << "operator new p:" << p << endl;
return p;
}
private:
int mb;
};
//A a; 4个字节
//B b; 8个字节
int main()
{
A *p = new B();//B::vftable
cout << "main p:" << p << endl;
p->func();
delete p;

return 0;
}

0115D9B8为分配的内存的起始地址,我们用基类指针指向派生类对象一定是指向派生类内存基类的起始部分:0115D9C0刚好比0115D9B8多了8个字节,是跨越了vbptr与mb,但是delete时候从0115D9C0开始释放,因此崩溃

这里的解决办法:因为栈上开辟的内存,没有在堆中,所以出了作用域自己会析构,不需要delete,直接不理会即可!

注意👀:Windows的VS下这样写会出错,但是Linux下的g++delete时会自动偏移到new内存的起始部分,进行内存free(),不会出错。

还有一个小问题提醒一下:

这个vfptr在最上方的原因:是因为A类中原本就有虚函数,然后B类中继承A,B中也有虚函数,也是将重写的虚函数覆盖,不同的虚函数地址写在vftable中,B的vfptr 与A是相同的(原理可以看B的vfptr什么时候写入内存,与A的vfptr关系);除非A没有虚函数,B有虚函数,那么vfptr就会紧跟着mb;

菱形继承问题

多重继承好处:可以复用多个基类的代码到派生类中

多继承的问题:

①菱形继承问题:会导致派生类有多份间接基类的数据,可以采用虚继承来解决。 **A为B、C的基类,B从A单继承而来,C从A也是单继承而来;D是B和C多继承而来,D有两个基类分别为B和C。A称为D的间接基类,D也有A的数据。**如图:

半圆形继承问题: **B从A单一继承而来,C有一个基类B而且同时还从A继承而来。A到B为单继承,C为多继承。**如图:

重点:多重继承虽然可以复用多个基类的代码到派生类中,但是会出现许多问题,因此C++开源代码上很少见到多重继承,我们也尽量少用多继承!

复现一下菱形问题:

class A
{
public:
A(int data):ma(data)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
protected:
int ma;
};

class B : public A
{
public:
B(int data):A(data), mb(data)
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
protected:
int mb;
};

class C : public A
{
public:
C(int data):A(data), mc(data)
{
cout << "C()" << endl;
}
~C()
{
cout << "~C()" << endl;
}
protected:
int mc;
};

class D : public B , public C
{
public:
D(int data):B(data), C(data), md(data)
{
cout << "D()" << endl;
}
~D()
{
cout << "~D()" << endl;
}
protected:
int md;
};

int main()
{
D d(10);

return 0;
}

内存结构图和结果图:

可以看出,D内存为20个字节,构造了两次A,存在两份A类数据;

解决方案:虚继承

class A
{
public:
A(int data):ma(data)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
protected:
int ma;
};

class B : virtual public A
{
public:
B(int data):A(data), mb(data)
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
protected:
int mb;
};

class C : virtual public A
{
public:
C(int data):A(data), mc(data)
{
cout << "C()" << endl;
}
~C()
{
cout << "~C()" << endl;
}
protected:
int mc;
};

class D : public B , public C
{
public:
D(int data):A(data), B(data), C(data), md(data)//记住,此时构造A,不属于B类,不属于C类,而是属于D类!!!
{
cout << "D()" << endl;
}
~D()
{
cout << "~D()" << endl;
}
protected:
int md;
};

int main()
{
D d(10);

return 0;
}

分析:B从A虚继承而来,A为虚基类,那么Ama移动到派生类最后面,在Ama位置上补一个vbptr;C也是从A虚继承而来,Ama移动到派生类最后面,但发现已经有一份同样的虚基类的数据,那么C的Ama丢弃,在Ama位置存放vbptr。派生类中只有一份基类Ama的数据,以后访问都是同一个ma;同时ma的初始化由D来负责。 虚继承就可以解决多重继承中的菱形继承与半圆形继承出现的问题了。

内存图和结果:

回答多继承问题:要回答到好处以及产生的问题,还有解决方案;

C++的四种类型转换

①const_cast

const_cast:修改类型的const或volatile属性。 使用该运算方法可以返回一个指向非常量的指针(或引用)指向,就可以通过该指针(或引用)对它的数据成员任意改变。

例子:

const int a = 10;
int *p1 = (int*)&a;//C中类型转换
int *p2 = const_cast<int*>(&a);//C++中类型转换const_cast

转换为相同类型的时候,我们通过反汇编查看时候,发现C中的类型强转与C++中const_cast所生成的汇编指令底层是一模一样的。

那么问题来了,两者那么在什么时候进行区分呢?解:不是在汇编,而是编译阶段进行区分;

注意👀: 1.类型要保持一致才可以进行合理的类型转换。

*const_cast使用时,地址的类型是与左边类型以及转换的类型需要保持一致!!!如:不能将const int 转换成char *,只能const int * 转成 int *; **

const int a = 10;
char *p1 = (char*)&a;//C中类型转换,可以
char *p2 = const_cast<char*>(&a);//C++中类型转换const_cast,不可以

2.const_cast<里面必须是指针或引用类型>

const int a = 10;
int b = const_cast<int>(a);//编译出错

②static_cast

static_cast(静态):编译时期的类型转换,提供编译器认为安全的类型转换。 是一个c++运算符,功能是把一个表达式转换为某种类型,使用最多!!!

注意: 1.有联系的类型之间可以互相转换。

int a = 10;
char b = static_cast<int>(a);//int 和 char 有联系,且都是四个字节;

2.没有任何联系的类型之间的转换会被否定。

int *p = nullptr;
//double* = (double*)p;C可以转换
double* b = static_cast<double*>(p); //没有联系,会被否定,且double为8字节,int为4字节;

3.基类类型与派生类类型进行转换,可以用static_cast,它们类型之间有关系,一定能转换成功,但不一定安全。

因为可能基类转换成派生类类型,访问了不合理的内存;

③reinterpret_cast

reinterpret_cast:类似于C风格的强制类型转换,是C++里的强制类型转换符,不安全。

注意: 1.如果非要进行没有任何联系的类型转换,可以使用reinterpret_cast

int *p = nullptr;
double* b = reinterpret_cast<double*>(p);//成功,但是不安全;

④dynamic_cast

dynamic_cast(动态):运行时期的类型转换,用于继承结构中,可以支持RTTI类型识别的上下转换及识别。!!!!将一个基类对象指针(或引用)转换到继承类指针,dynamic_cast会根据基类指针是否真正指向继承类指针来做相应处理。

例子:

class Base
{
public:
virtual void func() = 0;
};

class Derive1 : public Base
{
public:
void func()
{
cout << "call Derive1::func" << endl;
}
};

class Derive2 : public Base
{
public:
void func()
{
cout << "call Derive2::func" << endl;
}
};

void showFunc(Base* p)
{
p->func();//动态绑定
}

int main()
{
Derive1 d1;
Derive1 d2;
showFunc(&d1);
showFunc(&d2);

return 0;
}

这是之前经典的动态转换,结果为:

但是在实际中,需求是不断变化的,现在需要在Derive中实现一个新功能的API接口:

class Derive2 : public Base
{
public:
void func()
{
cout << "call Derive2::func" << endl;
}
//需求更改 Derive2实现新功能的API接口函数
void derive02func()
{
cout << "call derive02func()::func" << endl;
}
};

需求为: 我们的void show()应该区分判断一下,如果Base* p指向了其他的派生类对象,调用p->func()方法就好。但如果指向Derive2对象,不调用func()方法,而调用Derive2的derive02func()方法。如何操作呢?

这里就要识别*p的类型,看它指向哪个对象。此时就需要我们的dynamic_cast()了。dynamic_cast会检查p指针是否指向的是一个Derive2类型的对象;p->vfptr->vftable RTTI信息 如果是dynamic_cast,转换类型成功,返回Derive2对象地址;否则,返回nullptr。

void showFunc(Base* p)
{
//dynamic会检查p指针是否指向的是一个Derive2类型的对象
//p->vfptr->vftable RTTI信息 如果是dynamic_cast中的Derive2类型,转换类型成功,返回Derive2对象地址
//否则,返回nullptr
Derive2 *pd2 = dynamic_cast<Derive2*>(p);
if (pd2 != nullptr)
{
pd2->derive02func();
}
else
{
p->func();//动态绑定
}
}

更改代码后:调用成功

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: