首页 > 编程知识 正文

C 普通继承和虚继承详解,虚继承和普通继承区别

时间:2023-05-06 08:09:46 阅读:225610 作者:922

继承

继承概念

所谓的继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,被称为子类或者派生类,被继承的类称为父类或者基类。

继承特点

子类拥有父类的所有属性和方法(除了构造函数和析构函数)。子类可以拥有父类没有的属性和方法。子类是一种特殊的父类,可以用子类来代替父类。子类对象可以当做父类对象使用。

继承格式

class 派生类类名: 继承方式 基类名
{
成员变量和成员函数的声明
}
继承方式: public protected private

不同继承方式对子类访问的影响

总结

基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public >protected > private。使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。友元关系不能继承,也就是说基类友元不能访问子类的私有和保护变量。

基类和派生类对象赋值转换

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。基类对象不能赋值给派生类对象。(可以理解为父类的结构小于子类,子类能切割和父类相同大小的结构给父类赋值,而父类不能满足子类的大小所以无法给子类赋值)基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。 class Father{protected : int _age; // 年龄};class Son : public Father{public : int _Nu ; // 学号};void Test (){ Son s1 ; // 1.子类对象可以赋值给父类对象/指针/引用 Father f1 = s1; Father *f2 = &s1; Father& f3 = s1; //2.基类对象不能赋值给派生类对象 s1 = f1; // 3.基类的指针可以通过强制类型转换赋值给派生类的指针 Father *f2 = &s1; Son* s2 = (Son*)f2; //把父类指针强转为子类指针 s2->_Nu = 10;

继承中的作用域

在继承体系中基类和派生类都有独立的作用域。子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 来进行显示访问)成员函数的隐藏,子类只需要和父类函数名相同就能构成隐藏。注意在实际中在继承体系里面最好不要定义同名的成员。 class Father{public:int _num = 100;};class Son : public Father{public:void p(){cout << _name << " " << _num << endl; //这样只会输出子类的 _num,因为变量名相同子类会屏蔽父类cout << _name << " " << Father::_num << endl; // 想要输处父类的 _num 需要再变量前加 基类::基类}string _name = "Hi";int _num = 10;};int main(){Son s;s.p();}

派生类的默认成员函数

派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。(如果我们想构建一个不能被继承的类就可以把父类的构造函数私有化)派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。派生类的operator=必须要调用基类的operator=完成基类的复制。派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。派生类对象初始化先调用基类构造再调派生类构造。派生类对象析构清理先调用派生类析构再调基类的析构。

继承与静态成员

基类定义的 static 静态成员,则整个继承体系里面有且只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。

class Person{public: Person() { ++_count; }public: static int _count; // 统计个数。};int Person::_count = 0;class Student : public Person{protected: int _stuNum; };class Graduate : public Student{protected: int _GrdNum;};int main(){ Student s1; Student s2; Student s3; Graduate s4; cout << " 人数 :" << Person::_count << endl; // 4 Student::_count = 0; cout << " 人数 :" << Person::_count << endl; // 0 return 0;}

菱形继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

2.多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

3.菱形继承:菱形继承是多继承的一种特殊情况

4.菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份。

解决方式:

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在 Student 和 Teacher 的继承 Person 时使用虚拟继承(继承方式前面加 virtual),即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

一、虚继承原理

虚继承用于解决多继承条件下的菱形继承问题(数据冗余、存在二义性)。
底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(也被称作虚基表,不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。实际上,vbptr 指的是虚基类表指针(virtual base table pointer,也叫虚基表指针),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

二、虚基类的声明和语法形式:

class 派生类名:virtual 继承方式 基类名

三、虚基类的使用要点:

一个类可以在一个类族中用作虚基类,也可以用作非虚基类。在派生类的对象中,同名的虚基类只产生一个虚基类子对象,而某个非虚基类产生各自的对象。虚基类子对象是由最派生类(最后派生出来的类)的构造函数通过调用虚基类的构造函数进行初始化 (最派生类会先去调用虚基类的构造函数)。最派生类是指在继承类结构中建立对象时所指定的类。在派生类的构造函数的成员初始化列表中,必须列出对虚基类构造函数的调用,如果没有列出,则表示使用该虚基类的缺省构造函数。在虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中,都要列出对虚基类构造函数的调用。但只有用于建立对象的最派生类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类构造函数的调用在执行中被忽略,从而保证对虚基类子对象只初始化一次。在一个成员初始化列表中,同时出现对虚基类和非虚基类构造函数的调用时,基类的构造函数先于非虚基类的构造函数执行。虚基类并不是在声明基类时声明的,而是在声明派生类是,指定继承方式时声明的。因为一个基类可以在生成一个派生类作为虚基类,而在生成另一个派生类时不作为虚基类。

四、虚基表指针的存放位置

- 虚基表指针是存放在数据段的- 虚基表指针是放在对象的开头的

虚继承的使用例子

// 不使用虚继承class A{public:A(string s1){cout << s1 << endl;}};class B : public A{public:B(string s1, string s2):A(s1){cout << s2 << endl;}};class C : public A{public:C(string s1, string s3):A(s1){cout << s3 << endl;}};class D : public B, public C{public:D(string s1, string s2, string s3, string s4) //这里就和继承顺序有关系了: C(s1, s3), B(s1, s2) //D 是普通继承,所以D中即存在B也存在C,所以D在构造时会根据继承顺序先去调B的构造,而B会在构造自身时先去调用A的构造,C也一样,所以输出顺序为 A、B、A、C、D{cout << s4 << endl;}};int main(){D d("A", "B", "C", "D");return 0;}**************************************************//使用虚继承class A //此时 A 类也被称作 虚基类{public:A(string s1):_s1(s1){cout << s1 << endl;}string _s1;};class B :virtual public A //B使用了虚继承,B中包含 vbptr(A的虚基表指针)、_s1、_s2{public:B(string s1, string s2):A(s1), _s2(s2){cout << s2 << endl;}string _s2;};class C : virtual public A //C使用了虚继承,C中包含 vbptr(A的虚基表指针)、_s1、_s3{public:C(string s1, string s3):A(s1), _s3(s3){cout << s3 << endl;}string _s3;};class D : public B,public C //D是普通继承,所以D中包含了一个B(vptr(A的虚基表指针)、_s2)、C(vptr(A的虚基表指针)、_s3)、_s1 和 _s4,{public:D(string s1, string s2, string s3, string s4) //在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员并且虚基类子对象是由最派生类(最后派生出来的类)的构造函数通过调用虚基类的构造函数进行初始化//所以这里会先根据D中的 A(s1)去构造D中的A类对象_s1,然后会再次根据继承的顺序依次去构造B和C,因为D中的_s1独有一份(A输出什么只跟A(s1)中传入的s1相关和B、C中的第一参无关),所以B、C中不会再次去构建_s1,最终输出顺序为 A、B、C、D:C(s4, s3), A(s1), B(s3, s2), _s4(s4){cout << s4 << endl;}string _s4;};int main(){D d("A", "B", "C", "D");return 0;}**************************************************//不使用虚继承派生类结构体的大小class A{ protected:int _d;};class B : public A{ protected:int _d1;};class C : public A{ protected:int _d2;};class D : public B, public C{ protected:int _d3;};int main(){D c; cout << sizeof(c) << endl; // 20,因为没使用虚继承,B、C中除了自己原有的成员变量之外还各自继承了A中的成员变量,D继承了B、C后,除了自身的成员变量外还继承了B、C的成员变量,所以它里面有 _d1,_d2,_d1,_d3,_d4}*************************************************class A{protected:int _d;};class B :virtual public A // _d _d1 vbptr{protected:int _d1;};class C : virtual public A // _d _d2 vbptr{protected:int _d2;};class D : public B, public C // _d (_d1 vbptr) (_d2 vbptr) _d3{protected:int _d3;};int main(){D d; // 可以看出 d 中除了 4个 int 还有两个 vbptr(虚基表指针,一个4字节) 所以共 24个字节cout << sizeof(d) << endl; //24}

继承和组合

public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。优先使用对象组合,而不是类继承 。(因为继承中一个基类的改变会影响派生类的改变(破坏了类的封装),而组合却不会,组合容错率更好)继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系既可以用继承也可以用组合的话,就用组合。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。