c++之继承--单继承(多继承)、虚继承

xiaoxiao2021-02-28  52

继承

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。简单来说,继承就是在获取“父辈”东西的基础上,有选择的增添自己的东西。

比如手机功能的更迭:

继承的格式

继承的方式: 总结:

基类private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象 protected/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是 has-a 的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继承。私有继承意味着is-implemented-in-terms-of(是根据……实现的)。通常比组合(composition)更低级,但当一个派生类需要访 问基类保护成员或需要重定义基类的虚函数时它就是合理的 不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,基类的私有成员存在但是在子类中不可见(不能访问)使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式在实际运用中一般使用都是public继承,极少场景下才会使用protetced/private继承

派生类默认成员函数 继承体系下,派生类中如果没有显示定义这六个成员函数,编译器则会合成这六个默认的成员函数 【派生类对象的构造与析构】 继承的分类

这里只把<有无重写>放到虚拟继承不够准确,因为重写的要求如下: 1. 基类中被重写的函数必须为虚函数(先知道,就是在函数前面加虚拟virtual关键字) 2. 基类和派生类中的虚函数的原型的原型必须保持一致(返回值类型,函数名(参数列表)),即完全一样。那么普通继承里,自然也可以有重写。

class father { public: virtual void func1() { cout<<"father::func1()"<<endl; } private: int _a; }; class child:public father { public: //子类重写了父类的func1函数 virtual void func1() { cout<<"child::func1()"<<endl; } private: int _b; };

所以①②两个分类之间的继承方式,可以组合,比如有一下组合方式: 普通单继承无重写、普通单继承有重写、虚拟单继承有重写、虚拟单继承无重写、 普通多继承无重写、普通多继承有重写、虚拟多继承有重写、虚拟多继承无重写

先来说下普通继承吧:

class father { public: father() { _a=10; } void func1() { cout<<"father::func1()"<<endl; } private: int _a; }; class child:public father { public: child() { _b=20; } void func2() { cout<<"child::func2()"<<endl; } private: int _b; }; void test() { father a; child b; cout<<"sizeof(father):"<<sizeof(father)<<endl; cout<<"sizeof(child):"<<sizeof(child)<<endl; } int main() { test(); return 0; }

运行结果: 可见子类继承了父类的成员,再加上自己的成员变量,一共8字节。

而如果将成员函数改成虚函数:

class father { public: father() { _a=10; } virtual void func1() { cout<<"father::func1()"<<endl; } private: int _a; }; class child:public father { public: child() { _b=20; } virtual void func2() { cout<<"child::func2()"<<endl; } private: int _b; }; void test() { father a; child b; cout<<"sizeof(father):"<<sizeof(father)<<endl; cout<<"sizeof(child):"<<sizeof(child)<<endl; } int main() { test(); return 0; }

运行结果:

父类和子类都多了4个字节,查看内存:

接下来,介绍一位关键人物:“菱形继承” 它包含了单继承和多继承

class B { public: int _b; }; class C1:public B { public: int _c1; }; class C2 :public B { public: int _c2; }; class D : public C1, public C2 { public: int _d; }; void test1() { D d; d._b=1; //<-------- d._c1=3; d._c2=4; d._d=5; } int main() { test1(); return 0; }

这时候会报错:

类D继承自类C1和类C2,而C1类和C2类都继承自类B,则类D中会两次继承B。而直接对_b赋值,编译器会不知道是给从C1中继承的_b赋值,还是给从C2中继承的_b赋值。 所以,为了解决这个问题,有如下做法

void test1() { D d; d.C1::_b=1; //在变量前面加上作用域限定 d.C2::_b=2; //明确访问的变量 d._c1=3; d._c2=4; d._d=5; }

而更关键的是,为了节省空间,同时解决上面继承中的二义性的问题,我们可以将C1、C2对B的继承定义为虚拟继承,而B就成了虚拟基类。

关于虚拟继承,我参考的是这篇博客:【C++】简单介绍虚拟继承

虚拟继承与普通继承的区别:

1.书写形式上:虚拟继承要加虚拟关键字virtual

2.对象模型区别:虚拟继承要多四个字节空间,多的四个字节为偏移量表格的地址

如下,“菱形虚拟继承”:

class B { public: virtual void Funtest1() { cout << "B::Funtest1()" << endl; } virtual void Funtest2() { cout << "B::Funtest2()" << endl; } public: int _b; }; class C1:virtual public B { public: virtual void Funtest1() { cout << "B::Funtest1()" << endl; } virtual void Funtest3() { cout << "C1::Funtest3()" << endl; } public: int _c1; }; class C2 :virtual public B { public: virtual void Funtest2() { cout << "B::Funtest2()" << endl; } virtual void Funtest4() { cout << "C2::Funtest4()" << endl; } public: int _c2; }; class D : public C1, public C2 { virtual void Funtest8() {} virtual void Funtest7() {} public: int _d; };

插叙: 关于虚表的生成:

基类虚表的生成: 将类中的虚函数按照在类中的声明的先后次序添加到虚函数表中<单继承–派生类 > 如果派生类重写了基类的某个虚函数,使用派生类自己的虚函数,替换派生类虚表中相同偏移量位置 基类虚函数 ,然后再按照派生类特有的虚函数的声明次序,将其增加到虚函数表的最后(有些绕,仔细理解下)。特别注意:<多继承> 将派生类新增加的虚函数放置到第一张虚表的最后 这样会方便调用(见下菱形虚拟继承的对象模型)

虚函数的调用原理:

从对象的前4个字节取虚表的地址传递this指针从虚表中取对应的虚函数 :虚表地址 + 偏移量执行当前虚函数

正题:

菱形虚拟继承的内存分析:

菱形虚拟继承的对象模型:

可以看到,最后只继承了一次基类B,对成员变量_b的访问也明确了。

转载请注明原文地址: https://www.6miu.com/read-2619881.html

最新回复(0)