C++ 继承

mac2026-04-04  6

目录

前言

继承

         继承方式

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

继承中的作用域

派生类的默认成员函数

继承与友元

继承与静态成员

菱形继承与虚拟继承

虚拟继承

虚拟继承解决数据冗余和二义性的原理

继承和组合

小结


前言

C++三大特性: 封装, 继承, 多态

也是面向对象的三大特性

说明 : 本文程序都是在vs2017下的x86下调试运

继承

简单来说, C++允许一个类继承另一个类的全部类容, 提高了代码的复用性, 但破坏了封装性,增加了耦合性, 那么一个类怎样继承另一个类呢?

格式 :class 派生类名 : 继承方式 基类1 , 继承方式 基类2... {

};

如下:

#include<iostream> using namespace std; #include<string> class Base { int m_b; public: Base() :m_b(10) {} void Func(){ cout << "Base::Func()" << endl; } }; class Derive : public Base { int m_d; public: Derive() :m_d(20) {} void func() { cout << "Derive::func()" << endl; } }; int main(){ Base b; Derive d; system("pause"); return 0; }

我们在VS下调试可以看到, 对象d中确实继承了Base类的成员变量和成员函数    

前面在定义派生类时, 用到了继承方式, 在程序中我写的是public,  其实还有private和protected共三种继承方式, 这三个单词也是类中的访问限定符.  类的访问限定符 戳链接( ̄︶ ̄)↗

继承方式

public,  private和protected在作为继承方式时,  是对派生类继承来的基类成员进一步限定

继承方式基类public成员基类private成员基类protected成员public派生类中为public派生类中存在但不可见派生类中为protectedprivate派生类中为private派生类中存在但不可见派生类中为privateprotected派生类中为protected派生类中存在但不可见派生类中为protected

说明 :  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里还是类外都不能去访问它。这里的类内不能访问是指不能直接访问, 但可以通过继承来的接口函数访问. 2. protected成员的存在就是为了继承, 没有继承关系时和private没有区别 ,当基类中的成员不想在类外直接访问,但又想让继承后其派生类里可以访问, 就应该定义为protected成员protected成员就可以理解为可以被继承的private成员.  3. 基类成员在派生类中的访问权限 = Min(基类中的访问权限,  继承方式) , public>protected>private  4. 继承时也可以不写继承方式, class默认是private继承, struct默认是public继承


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

1. 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割. 意思是把派生类中基类那部分切下来成为一个独立的基类, 如下图

2. 基类对象和引用 不能赋值给派生类对象

基类的指针可以通过强制类型转换赋值给派生类的指针, 但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI的dynamic_cast 来进行识别后进行安全转换

例如下面的例子 :  

#pragma once #include<iostream> #include<string> using namespace std; class Person { string m_name; bool m_sex; int m_age; public: Person(string name, bool sex, int age) :m_name(name), m_sex(sex), m_age(age) {} Person(Person& x) :m_name(x.m_name), m_sex(x.m_sex), m_age(x.m_age) {} void Print_Peson() { cout << "姓名: " << m_name << " 性别: "; m_sex ? cout << "男" : cout << "女"; cout << " 年龄: " << m_age; } }; class Student : public Person { int m_stunum; public: Student(string name, bool sex, int age, int stunum) :m_stunum(stunum), Person(name, sex, age) {} void Print_Student() { Print_Peson(); cout << " 学号: " << m_stunum << endl; } }; #include<iostream> #include"class.h" using namespace std; void test1() { Student* Sp = new Student("张三", true, 20, 1901); Person* Pp = Sp;//给指针 Person& Pr = *Sp;//给引用 Person a = *Sp;//给对象 Pp->Print_Peson(); cout << endl; Pr.Print_Peson(); cout << endl; a.Print_Peson(); cout << endl; } void test2() { Person* Pp = new Person("李四", false, 19); Student* Sp = (Student*)Pp; Sp->Print_Student(); } int main() { test1(); test2(); system("pause"); return 0; }

可以看到, 结果是正如前面所说的, 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用, 基类的指针可以通过强制类型转换赋值给派生类的指针, 但不能访问派生类自己的成员, 可以看到访问到了非法内存


继承中的作用域

我们知道, 类本身就是一个作用域, 那么当一个类继承了另一个类时, 作用域又将如何变化呢?

1. 继承关系发生后, 基类和派生类还是有各自独立的作用域

2. 当基类和子类有重名成员时, 派生类中将屏蔽掉基类的重名成员, 这种现象叫隐藏, 也叫重定义     解释:   1). 重名, 对变量来说, 只需要变量名重名, 与变量类型无关, 对函数来说, 只需要函数名重名, 与返回值与参数列表无关   2). 屏蔽, 指的是, 当在派生类中直接访问重名成员时, 访问的都是派生类的

3. 事实上基类的重名成员只是被隐藏了, 我们还是可以通过 基类类名::重名成员  这种方式来访问

4. 在实际继承关系中, 最好不要定义重名成员

来看个例子

#include<iostream> using namespace std; class Base { public: int m_data; Base():m_data(10){} int func() { cout << "Base的func()" << endl; return 0; } }; class Derive : public Base { public: double m_data; Derive() :m_data(20.0) {} void func() { cout << "Derive的func()" << endl; } }; int main() { Derive d; cout << d.m_data << endl; d.func(); cout << d.Base::m_data << endl; d.Base::func(); system("pause"); return 0; }


派生类的默认成员函数

就算一个啥成员也没有的类, 其实也有6个默认成员函数, 就是我们不定义, 编译器会自己生成的6个函数

那么, 当继承关系发生后, 派生类中的默认成员函数会不会有啥特别的操作? 答案是, 会的, 如下

1. 派生类默认构造会先调基类的默认构造初始化基类的成员, 如果基类没有默认构造(没有无参构造), 那么派生类构造必须要以初始化列表的形式调用基类构造

2. 派生类的默认构造函数会先调基类的构造构造, 先完成基类的拷贝, 再完成自己的

3. 派生类的默认operator=会先调基类的operator=, 先完成基类的赋值, 再完成自己的

4.派生类析构会先调派生类的析构, 再调基类的析构

问题 :

实现一个不能被继承的类

方法一: C++11 新添加了final关键字, 直接在定义类时加在类名后面, 如下:

class A final{ };

方法二: 将类的构造函数或者析构函数私有化,  这个类将不能被继承

缺点, 当类的构造函数或析构函数私有化之后, 这个类就只能在堆上创建对象了, 私有化构造函数或析构函数戳链接( ̄︶ ̄)↗

如下:

构造函数私有化, 两个静态成员函数用于在堆上创建对象

class T1 { T1(int val):b(val) { } T1(T1& x):b(x.b){ } public: static T1* newT1_p(int val = 0) { return new T1(val); } static T1& newT1(int val = 0) { return *new T1(val); } };

析构函数私有化, 需要有一个public的函数调用析构, 最后由我们手动调用

class T2 { ~T2() { delete this; } public: int b; T2(int val = 0) :b(val) { } void Destroy() { this->~T2(); } };

继承与友元

友元不会被继承

也就是说, 不管是基类的友元类还是基类的友元函数, 都不是派生类的友元


继承与静态成员

基类中有static静态成员 , 不管怎样被继承, 继承多少,个 几重继承, 继承体系中都只有这一份静态成员


菱形继承与虚拟继承

多继承就是一个派生类继承了多个基类,  菱形继承是多继承的特殊情况

图示 :

当一个子类继承的多个父类中,  有两个以上的父类是同一个爷爷类(父类的的父类, 这里为了区分, 叫爷爷类), 则此时就发生了菱形继承,  如上图, D类中就有两份A类的内容,  造成了数据冗余性二义性问题

冗余性: 在D中有两份A类的数据 二义性: 访问D中A类的数据使, 不知道要访问从B中继承来的, 还是从C中继承来的  二义性测试, 如下代码

#include<iostream> using namespace std; class A { int m_a; }; class B : public A { int m_b; }; class C : public A { int m_c; }; class D : public B, public C{ int m_c; }; int main() { D d; d.m_a = 10; cout<<d.m_a<<endl; system("pause"); return 0; }

错误提示, 访问不明确, 因为编译器也不知道到底要访问B继承的还是A继承的

为了解决菱形继承中的数据冗余性和二义性的问题, C++中提供了虚拟继承

虚拟继承

在继承基类时加virtual 关键字, 如下 , 就说派生类, 虚拟继承了基类

class 派生类 : virtual 继承方式 基类, ...{

};

我们把上面B和C继承A改为虚拟继承, 再次测试如下

#include<iostream> using namespace std; class A { public: int m_a; }; class B :virtual public A { public: int m_b; }; class C :virtual public A { public: int m_c; }; class D : public B, public C{ public: int m_c; }; int main() { D d; d.m_a = 10; cout << d.m_a << endl; system("pause"); return 0; }

可以看到, 没有二义性的问题, 实际上现在, D类中只有1份A类的成员, 二义性, 冗余性都被解决了  

虚拟继承解决数据冗余和二义性的原理

实际在添加virtual关键字之后, B和C就是虚拟继承A了, 虚拟继承和普通继承就有一点区别, 虚拟继承之后子类中多了一个隐藏的虚基表, 放着虚基类指针, 作用是标识自己的父类,  也就是B和C中多了一个虚基表, 这个表中的虚基类指针指向自己的父类A,  当D继承B和C时, 发现B和C的基类都是A, 那么D在继承时也会只继承一份A

所以, 虚拟继承一般只用于菱形继承, 因为引入的虚基表会带来额外开销

继承和组合

优先使用对象组合,而不是类继承。

                                        

哈哈哈, 不推荐使用可不代表就不需要掌握啦, 该会还得会

参考 : https://www.cnblogs.com/nexiyi/archive/2013/06/16/3138568.html 

 继承和组合的比较:

  面向对象系统中功能复用的两种最常用技术是类继承和对象组合(object composition)。正如我们已解释过的,类继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,父类的内部细节对子类可见。

  对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。

        继承和组合各有优缺点。类继承是在编译时刻静态定义的,且可直接使用,因为程序设计语言直接支持类继承。类继承可以较方便地改变被复用的实现。当一个子类重定义一些而不是全部操作时,它也能影响它所继承的操作,只要在这些操作中调用了被重定义的操作。

        但是类继承也有一些不足之处。首先,因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现。更糟的是,父类通常至少定义了部分子类的具体表示。因为继承对子类揭示了其父类的实现细节,所以继承常被认为“破坏了封装性” 。子类中的实现与它的父类有如此紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。当你需要复用子类时,实现上的依赖性就会产生一些问题。如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现。

        对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。组合要求对象遵守彼此的接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。这还会产生良好的结果:因为对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运行时刻还可以用一个对象来替代另一个对象;更进一步,因为对象的实现是基于接口写的,所以实现上存在较少的依赖关系。

  对象组合对系统设计还有另一个作用,即优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。另一方面,基于对象组合的设计会有更多的对象 (而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。

  这导出了我们的面向对象设计的第二个原则:优先使用对象组合,而不是类继承。

来看个例子

#include<iostream> using namespace std; class A { public: int m_a; }; class B { public: A m_A; int m_b; }; class C { public: B m_B; int m_c; }; int main() { C c; c.m_B.m_A.m_a = 10; c.m_B.m_b = 20; c.m_c = 30; system("pause"); return 0; }

调试, 看一下对象的值,  可以看到这种组合方式和继承一样都得到两一个类的全部内容


小结

1.在实际应用中,  一般不建议多继承, 一定不要菱形继承

2. 继承提高了代码复用性(白箱风格复用), 但在一定程度上破坏了封装性, 增加了耦合性

3. 优先使用对象组合(黑箱风格复用), 而不是继承, 对象组合要求被组合的对象具有良好定义的接口. 对象组合耦合度低, 封装性完整, 代码维护性好

4. 能继承能组合尽量用组合, 但有些情况只能用继承, 例如三大特性之一, 多态的实现

 

 

最新回复(0)