深入解析C++继承体系:从访问控制到虚继承的全面指南

目录

一、什么是继承

1.1 继承的概念和定义

1.2 9种组合方式

1.3 继承模板

二、继承中基类和派生类的转换

2.1 派生类转换为基类

2.2 基类不能直接转换为派生类

三、继承中的作用域

3.1 隐藏规则

3.2 隐藏和重载的区别

四、继承中默认成员函数

五、实现不能继承的类

六、继承中多继承

6.1 单继承和多继承

6.2 菱形继承

6.3 虚继承

6.4 虚继承和菱形继承的区别总结

七、继承和组合

7.1 继承的使用

7.2 组合的使用

7.3 区别

八、总结(继承的作用)

一、什么是继承

1.1 继承的概念和定义

继承是面向对象编程的核心特性之一,它允许一个类(派生类/子类)获取另一个类(基类/父类)的属性和行为。派生类能继承基类中的所有成员变量和成员函数,但是友元关系不能继承(就像你把的朋友不能继承他家财产),这里就需要自己在继承内加入友元关系。这里需要友元两个类,而编译器会往上找,所以这里还需要声明另一个类(继承z中需要调用函数还需要声明一些函数,这里的函数是上面寻找不到的,也就是在此类后面定义的类,如果需要使用后面定义的类的函数,就得声明,使用别人类之前是友元)。

class Student;//(由于这里友元了函数有Student这个类,编译器向上寻找Student这个类定义,没找到只能直接前置声明) class Person { //友元 friend void DisplayDate(const Person& p, const Student& s); public: private: string _name; string _address; string _tel; int _age; }; //Student是派生类,Person是基类,这里public是继承方式 class Student: public Person { public: friend void DisplayDate(const Person& p, const Student& s); private: string _uid; }; void DisplayDate(const Person& p, const Student& s) { cout << p._name << endl; cout << s._uid << endl; cout << s._name << endl; }

注意:这里继承对于基类中的静态成员变量,是不算继承的,静态成员变量在整个继承体系中只有一份(可以这么理解,继承就是共享,而不是复制),基类的静态成员一定不包含在基类/子类中,它是属于栈外的,不属于某一个类。子类不一定比基类大,可能相同。

1.2 9种组合方式

类成员/继承方式public继承protected继承private继承
基类的public成员可以访问基类public成员不可访问不可访问
基类的protected成员可以访问基类protected成员可以访问基类protected成员不可访问
基类的private成员不可访问((抛开私有成员不能访问,public继承是可以访问private成员,但是这里受到private限制符限制不能被其他类访问))不可访问(同理)不可访问(同理)

注意:派生类在继承基类时,不一定需要明确指定继承方式(会默认为私有继承)。在类中封装protected和private是一样的,都是限制访问(不能在类外访问,只能类内访问),但是在继承中就能看出区别了,基类private成员是不能以任何继承方式被派生类访问,而基类protected成员是可以被派生类访问(这个在基类protected限制,在派生类中也能被使用,就是针对继承需求写的语法)。这里派生类访问基类受到继承方式和基类中限制符,继承方式中public最大,protect较大,private最小(啥也不能访问,只是继承所有成员变量/函数),这里public派生类可以访问基类public和protected,而protected只能访问到基类的protected)。

一句话总结,不管继承方式是什么,在内存中派生类都已经把基类中的所有成员和函数都继承下来了,只是继承方式对派生类访问/调用基类成员/函数限制(这个过程只受基类它给的限制符限制。

1.3 继承模板

类模板的继承,直接调用类模板中的函数,会导致编译错误,它这找不到识别标志,但是在一个类模板函数中实例化过程也会调用另一个类的实例化,这个过程的函数并未实例化所以编译器找不到,所以在调用基类模板函数时,需要显示实例化并指定该函数类域才能调用该函数并完成实例化。

#include<vector> template<class T> class stack : public vector<T> { public: void push(const T& value) { vector<T>::push_back(value);//继承中组合模板类时,需要调用模板类中的函数时需要实例化(stack实例化过程会带动vector实例化,但是vector的函数并未实例化) //push_back(value);//一般来说这样已经够了,因为是public继承,vector<T>中函数也是public,继承后可以直接调用,但是这里是模板类,函数未实例化 } void pop() { vector<T>::pop_back(); } T& top() { return vector<T>::back(); } const T& top()const { return vector<T>::back(); } bool empty()const { return vector<T>::empty(); //return empty();//这里也不能这样,编译器找不到,这里也存在隐藏(就是在基类中出现empty,然后再派生类也出现了empty,会先调用派生类的) } };

二、继承中基类和派生类的转换

2.1 派生类转换为基类

直接转换基类 = 派生类(子类的值给父类)这里实际上是派生类进行切片,转换为父类中,这个很好理解本来子类就是利用的父类继承下来的,类型当然也相同直接转换即可(不需要产生临时对象,也就不会在引用过程中担心权限问题)。也可以这样理解父亲赚到的钱,也会成为他儿子的钱,这样父亲的行为会影响儿子的结果,所以基类可以用引用/指针去指向派生类,这里也是多态的形成条件之一。

class Person { public: Person(string name = "xxxx", string address = "xxxxxxx", string tel="181XXX", int age = 0) :_name(name) ,_address(address) ,_tel(tel) ,_age(age) { } private: string _name; string _address; string _tel; int _age; }; class Student: public Person { public: Student(string name = "xxxx", string address = "xxxxxxx", string tel = "181XXX", int age = 0,string uid = "2403XXX") :Person(name,address,tel,age) ,_uid(uid) {} private: string _uid; }; int main() { Person p("张三", "翻斗花园1号", "110", 18); Student s("李四", "翻斗花园2号", "120", 20,"240312345"); Person& P = s;//直接切片 Person* P = &s;//直接切片 return 0; }

2.2 基类不能直接转换为派生类

在c++中基类对象不能强制赋值给派生类对象(派生类 = 基类),这是面向对象设计的基本原则。所以这里对于派生类去引用和指针指向基类对象是不行的(这里基类切片没意义,而且还会导致派生类中定义的私有成员未定义情况)。但是基类对象也不是不能转换为派生类,只是这里需要使用写构造函数/赋值构造来转换,而对于引用/指针就需要使用dynamic_cast来强制转换。

总结:

三、继承中的作用域

3.1 隐藏规则

1.在继承体系中基类(父)和派生类(子)都有独立的作用域

2.在派生类和基类中有同名成员,派生类成员将屏蔽基类同名成员,并直接访问派生类种的成员,这种叫隐藏(可以使用类域的一样,去强调这个属于哪个域的)

3.成员函数的隐藏,只要函数名相同且基类和派生类中都有就构成隐藏。

class A { public: void fun() { cout << "func()" << endl; } }; class B : public A { public: void fun(int i) { cout << "func(int i)" <<i<<endl; } };

这里也同样构成隐藏,满足函数名相同,并且在派生类和基类都有相同函数名(这里和有无参数无关系,只要相同函数即可),所以当我们创建一个B的对象时候,然后调用fun函数如果是没参数传过去会编译报错,它并不会去匹配A中的函数(因为被隐藏了)。

3.2 隐藏和重载的区别

先从概念入手,隐藏的概念:在继承中,派生类的函数会隐藏基类中同名的函数,只要函数名相同,不管参数列表是否相同,基类的函数都会被隐藏(除非使用using声明引入基类函数)。重载概念:在同一作用域内,函数名相同,参数列表不同(参数类型、个数、顺序),返回类型可以相同也可以不同。

从概念我们进行总结它们俩区别

特性重载隐藏
作用域必须在同一个作用域内在不同类域中(派生类隐藏基类函数)
函数名必须相同必须相同
参数列表必须不同没有要求(有无都不影响隐藏)
返回类型没有要求没有要求
virtual关键字没有要求没有要求
继承关系不需要继承必须存在继承

对于隐藏的导致不能使用基类函数,我们还是在继承中最好不要用同名/明确使用基类中隐藏的函数时候加入类域去访问该隐藏的函数去显式调用。

四、继承中默认成员函数

两份部分分开处理:各自处理各自。

构造函数/拷贝构造/赋值

1.基类(父)成员调用基类自己的构造/拷贝/赋值

2.派生类(子)成员调用自己的(也就是和之前学的类和对象一样)。在声明顺序是基类先于派生类,在调用初始化列表时会先调用基类成员(这里又因为我们是把基类看成整体,所以我们在给基类成员初始化/拷贝/赋值时,需要像匿名对象调用一样,把值传给基类,从而实现在派生类统一初始化/拷贝/赋值,当然前提是基类有这种构造函数/拷贝才能初始化/拷贝/赋值)

对于析构函数

析构函数名字实际上不是~类名,编译器会自动处理为Distractor,所以这里就涉及到隐藏,调用到相同名字,所以在调用基类析构时,需要指明基类中的调用。

class Student;//(由于这里友元了函数有Student这个类,编译器向上寻找Student这个类定义,没找到只能直接前置声明) class Person { //友元 friend void DisplayDate(const Person& p, const Student& s); public: Person(string name = "xxxx", string address = "xxxxxxx", string tel="181XXX", int age = 0) :_name(name) ,_address(address) ,_tel(tel) ,_age(age) { } ~Person() { cout << "~Person()" << endl; } private: string _name; string _address; string _tel; int _age; }; //Student是派生类,Person是基类,这里public是继承方式 class Student: public Person { friend void DisplayDate(const Person& p, const Student& s); public: Student(string name = "xxxx", string address = "xxxxxxx", string tel = "181XXX", int age = 0,string uid = "2403XXX") :Person(name,address,tel,age)//先调用基类构造,只能这样调用基类构造来初始化派生类 ,_uid(uid) {} ~Student() { Person::~Person();//这里编译器处理后析构函数统一为destractor,所以这里涉及隐藏,必须显式调用 cout << "~Student()" << endl; } private: string _uid; };

左是没在派生类调用~Person(),编译器自动释放的

右是在派生类析构中调用~Person(),会导致先初始化先析构的情况

当然这里派生类中不用写基类的析构函数,编译器为了避免内存泄漏和调用顺序(是先析构派生类在析构基类,这里因为基类先初始化,并且怕后续需要基类而提前释放空间对派生类的影响)的混乱,会等待派生类析构后自动调用基类的析构函数。

五、实现不能继承的类

方法一:可以把基类的构造函数私有化,因为派生类实例化必须先调用基类的构造函数。

class Person { private://把构造私有化,任何继承都不能访问私有成员,变现限制继承 Person(string name = "xxxx", string address = "xxxxxxx", string tel="181XXX", int age = 0) :_name(name) ,_address(address) ,_tel(tel) ,_age(age) { } private: string _name; string _address; string _tel; int _age; }; class Student: public Person { public: Student(string name = "xxxx", string address = "xxxxxxx", string tel = "181XXX", int age = 0,string uid = "2403XXX") :Person(name,address,tel,age) ,_uid(uid) {} private: string _uid; };

方法二:实现一个类不能继承可以在基类中类名加final。

class Person final//不能被继承 { public: Person(string name = "xxxx", string address = "xxxxxxx", string tel="181XXX", int age = 0) :_name(name) ,_address(address) ,_tel(tel) ,_age(age) { } private: string _name; string _address; string _tel; int _age; }; class Student: public Person { public: Student(string name = "xxxx", string address = "xxxxxxx", string tel = "181XXX", int age = 0,string uid = "2403XXX") :Person(name,address,tel,age) ,_uid(uid) {} private: string _uid; };

六、继承中多继承

6.1 单继承和多继承

单继承 是指一个派生类继承一个直接基类(和基类中包含继承没关系),多继承就是一个派生类只有两个或以上直接基类。左边就是单继承,右边是多继承

6.2 菱形继承

菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以

看出菱形继承有数据冗余和⼆义性的问题。
class Person { public: Person(string name = "xxxx", string address = "xxxxxxx", string tel="181XXX", int age = 0) :_name(name) ,_address(address) ,_tel(tel) ,_age(age) { } private: string _name; string _address; string _tel; int _age; }; //Student是派生类,Person是基类,这里public是继承方式 class Student: public Person { public: Student(string name = "xxxx", string address = "xxxxxxx", string tel = "181XXX", int age = 0,string uid = "2403XXX") :Person(name,address,tel,age) ,_uid(uid) {} private: string _uid; }; class Teacher : public Person { public: Teacher(string name = "xxxx", string address = "xxxxxxx", string tel = "181XXX", int age = 0, string office = "一教302") :Person(name, address, tel, age) ,_office(office) {} private: string _office; }; //菱形继承 class School :public Student, public Teacher { public: School(string name = "xxxx", string address = "xxxxxxx", string tel = "181XXX", int age = 0, string uid = "2403XXX", string office = "一教302") :Student(name, address, tel, age, uid) , Teacher(name, address, tel, age, office) {} private: string _school; };

这就是一个典型的菱形继承,导致数据冗余从而导致二义性问题,我们这里需要的是这两个继承下来的Person中的成员中的一个即可,不需要继承两个同样的Person成员。这里我们只需要解决其中一个数据冗余问题,这两个问题就能解决了。

6.3 虚继承

根据多继承可能出现的菱形继承,就一定会出现数据冗余和二义性问题(会导致内存浪费和数据不一致,这里数据不一致是不同路径的数据可能不同步,还有构造/析构开销),这里展开了一个虚继承,它是会吧这两个继承同一个基类的放到外面,然后通过指针等一些列操作去访问同一个该变量,从而避免了数据冗余和内存浪费。使用关键词virtual

class Person1 { public: Person1(const char* name) :_name(name) {} string _name; // 姓名 }; class Student1 : virtual public Person1 { public: Student1(const char* name, int num) :Person1(name) , _num(num) {} protected: int _num; //学号 }; class Teacher1 : virtual public Person1 { public: Teacher1(const char* name, int id) :Person1(name) , _id(id) {} protected: int _id; // 职工编号 }; class Assistant : public Student1, public Teacher1 { public: Assistant(const char* name1, const char* name2, const char* name3) :Person1(name1) , Student1(name2, 2) , Teacher1(name3, 3) , _majorCourse("xxxx") {} protected: string _majorCourse; // 主修课程 };

注意:在菱形继承中的初始化过程,它会以菱形继承中的根基类(以上面的代码为例就是Person1)的初始化为最终的初始化结果(这个结果只是影响根基类中的变量结果,但是不影响其他派生类中特有的变量结果)

6.4 虚继承和菱形继承的区别总结

关键区别:普通菱形继承的二义性:

原因是存在多个不同的 Base

对象编译器不知道你要操作哪个对象

必须明确指定访问路径

虚继承的路径调用:

所有路径指向同一个共享的 Base 对象

不同路径只是访问方式不同,操作的是同一数据

可以直接访问,也可以选择路径(但效果相同)

本质差异:普通菱形继承:"你有两个名字相同的亲戚,需要指定是哪一个"虚继承:"你有一个亲戚,可以用不同昵称称呼他"

七、继承和组合

7.1 继承的使用

#include<vector> template<class T> class stack:public vector<T> { public: void push(const T& value) { vector<T>::push_back(value);//继承中组合模板类时,需要调用模板类中的函数时需要实例化(stack实例化过程会带动vector实例化,但是vector的函数并未实例化) } void pop() { vector<T>::pop_back(); } T& top() { return vector<T>::back(); } const T& top()const { return vector<T>::back(); } bool empty()const { return vector<T>::empty(); } };

7.2 组合的使用

#include<vector> template<class T> class stack { public: void push(const T& value) { _v.push_back(value); } void pop() { _v.pop_back(); } T& top() { return _v.back(); } const T& top()const { return _v.back(); } bool empty()const { return _v.empty(); } private: vector<T> _v; };

7.3 区别

public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。
组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤
(white-box reuse)。术语“⽩箱”是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对派⽣类可
见 。继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依
继承缺点:赖关系很强,耦合度⾼。
对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对
象组合要求被组合的对象具有良好定义的接⼝。这种复⽤风格被称为⿊箱复⽤(black-box reuse),
因为对象的内部细节是不可见的。对象只以“⿊箱”的形式出现。
组合优点组合类之间没有很强的依赖关系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。
优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太
那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的
关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。
#include<vector> template<class T> class stack : public vector<T>//如果没有继承vector这里是可以正常实现stack栈的功能的,只是这里组合和继承一起了,还调用函数也混淆了才会导致栈的功能不正常(也就是数据混乱) { public: void push(const T& value) { _v.push_back(value);//插入到_v成员变量的内部去了,而不是stack这个栈中了 vector<T>::push_back(value);//正确调用,这才能插入到stack这个类中,而不是私有成员_v } void pop() { _v.pop_back(); } T& top() { return _v.back(); } const T& top()const { return _v.back(); } bool empty()const { return _v.empty(); } private: vector<T> _v; };

在日常中最好还是不要写上面这种代码,虽然也是可以的,但是容易混淆,就像这里混淆了,把_v当作stack的主体了,进行使用,但是这里stack又继承就会导致_v没意义了,它内部是以继承中的vector<T>为主体(包括_size,_capacity,_arr这些),当像上面的代码这样写,插入时候会插到_v中去了,不在stack主体上了(可以理解成隐藏了,但是不是隐藏,只是可以当成隐藏理解),而是它的成员内部去了,如果想用也是可以的,只不过这里需要把插入这些逻辑变成继承中的。

八、总结(继承的作用)

继承就是一个复用,可以使我们代码提供更简便。我们要理解继承方式和类中限定符的区别(protected用处是在继承中替代private的作用),不仅如此还要注意派生类和基类的转换(派生类可以直接赋值给基类也就能够是得基类直接引用和指针指向派生类)。继承中的隐藏也是一个很重要的知识点,这里又涉及到了和重载的区别。继承中的多继承是一个继承的多样化,但是我们需要注意多继承和单继承区别,还有就是出现多继承,必然可能会出现菱形继承,在实践中我们应该避免菱形继承,这种菱形继承会涉及到虚继承(虚表),从而解决数据冗余和二义性问题。在写代码的途中我也发现了继承和组合这两个一起时候,如果使用不当会造成实现结果不符合自己预期,要么就是避免两个相同类(一个组合一个继承)同时使用,要么就是逻辑需要以继承为主,避免以组合为主,导致我上面写出的栈功能不对。当然在既能使用组合和继承这两复用时候,我们可以首先选择组合,因为组合依赖性低,耦合度低。