C++学习之旅【C++继承概念指南与核心内容介绍】


🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》

《C++知识内容》《Linux系统知识》

✨逆境不吐心中苦,顺境不忘来时路! 🎬 博主简介:

引言:前篇文章,小编已经介绍了关于C++中模板进阶内容介绍!相信大家应该有所收获!接下来我将带领大家继续深入学习C++的相关内容!本篇文章着重介绍关于C++继承概念指南与核心内容介绍,那么这里面到底有哪些知识需要我们去学习的呢?废话不多说,带着这些疑问,下面跟着小编的节奏🎵一起学习吧!

目录

1.继承的概念及定义

1.1继承的概念

继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类.继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程.以前我们接触的函数层次的复⽤,继承是类设计层次的复⽤.下⾯我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类⾥⾯就是冗余的.当然他们也有⼀些不同的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣的独有成员函数是学习,⽼师的独有成员函数是授课.

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Student
{
public:
 //进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
 void identity()
 {
 // ...
 }
 // 学习
 void study()
 {
 // ...
 }
protected:
 string _name = "peter"; // 姓名
 string _address; // 地址
 string _tel; // 电话
 int _age = 18; // 年龄
 int _stuid; // 学号
};
class Teacher
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
 void identity()
 {
 // ...
 }
 // 授课
 void teaching()
 {
 //...
 }
protected:
 string _name = "张三"; // 姓名
 int _age = 18; // 年龄
 string _address; // 地址
 string _tel; // 电话
 string _title; // 职称
};
int main()
{
 return 0;
}

下⾯我们公共的成员都放到Person类中,Student和teacher都继承Person,就可以复⽤这些成员,就不需要重复定义了,省去了很多⿇烦.

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Person
{
public:
 // 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
 void identity()
 {
 cout << "void identity()" <<_name<< endl;
 }
protected:
 string _name = "张三"; // 姓名
 string _address; // 地址
 string _tel; // 电话
 int _age = 18; // 年龄
};
class Student : public Person
{
public:
 // 学习
 void study()
 {
 // ...
 }
protected:
 int _stuid; // 学号
};
class Teacher : public Person
{
public:
 // 授课
 void teaching()
 {
 //...
 }
protected:
 string title; // 职称
};
int main()
{
 Student s;
 Teacher t;
 s.identity();
 t.identity();
 return 0;
}

1.2继承定义

下⾯我们看到Person是基类,也称作⽗类.Student是派⽣类,也称作⼦类.(因为翻译的原因,所以既叫基类/派⽣类,也叫⽗类/⼦类).
可以用生活中的例子理解:
父类(基类/Base Class):比如动物类,包含所有动物共有的属性(如年龄、体重)和行为(如呼吸、进食).
子类(派生类/Derived Class):比如猫类,不需要重新定义动物的通用属性/行为,只需继承动物类,再添加猫特有的属性(如毛色)和行为(如抓老鼠).
简单来说:子类可以继承父类的成员(变量、函数),并可以扩展自己的专属功能.


1.3继承基类成员访问⽅式的变化

类成员/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 在派生类中不可见 在派生类中不可见 在派生类中不可见

1️⃣基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的.这⾥的不可⻅是指基类的私有成员
还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它.
2️⃣基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类中能访问,就定义为protected.可以看出保护成员限定符是因继承才出现的.
3️⃣实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅.基类的其他成员
在派⽣类的访问⽅式 == Min(成员在基类的访问限定符,继承⽅式),public > protected >private.
4️⃣使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显示的写出继承⽅式.
5️⃣在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实际中扩展维护性不强.

cpp 复制代码
//实例演示三种继承关系下基类成员的各类型成员访问关系的变化 
class Person
{
public :
 void Print ()
 {
 cout<<_name <<endl;
 }
protected :
 string _name ; // 姓名
private :
 int _age ; // 年龄
};
//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected :
 int _stunum ; // 学号
};

1.4继承类模板

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
using namespace std;
namespace lcz
{
 //template<class T>
 //class vector
 //{};
 // stack和vector的关系,既符合is-a,也符合has-a
 template<class T>
 class stack : public std::vector<T>
 {
 public:
 void push(const T& x)
 {
 // 基类是类模板时,需要指定⼀下类域,
 // 否则编译报错:error C3861: "push_back": 找不到标识符
 // 因为stack<int>实例化时,也实例化vector<int>了
 // 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
 vector<T>::push_back(x);
 //push_back(x);
 }
 void pop()
 {
 vector<T>::pop_back();
 }
 const T& top()
 {
 return vector<T>::back();
 }
 bool empty()
 {
 return vector<T>::empty();
 }
 };
}
int main()
{
 lcz::stack<int> st;
 st.push(1);
 st.push(2);
 st.push(3);
 while (!st.empty())
 {
 cout << st.top() << " ";
 st.pop();
 }
 return 0;
}

2.基类和派⽣类间的转换

1️⃣通常情况下我们把⼀个类型的对象赋值给另⼀个类型的指针或者引⽤时,存在类型转换,中间会产⽣临时对象,所以需要加const,如:int a = 1; const double& d = a; public继承中,就是⼀个特殊处理的例外,派⽣类对象可以赋值给基类的指针/基类的引⽤,⽽不需要加const,这⾥的指针和引⽤绑定是派⽣类对象中的基类部分,如下图所示.也就意味着⼀个基类的指针或者引⽤,可能指向基类对象,也可能指向派⽣类对象.
2️⃣派⽣类对象赋值给基类对象是通过基类的拷⻉构造函数或者赋值重载函数完成的(这两个函数的细节后⾯⼩节会细讲),这个过程就像派⽣类⾃⼰定义部分成员切掉了⼀样,所以也被叫做切割或者切⽚,如下图中所示.
3️⃣基类对象不能赋值给派⽣类对象.
4️⃣基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤.但是必须是基类的指针是指向派⽣类对象时才是安全的.这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time TypeInformation)的dynamic_cast 来进⾏识别后进⾏安全转换.(这个到后⾯类型转换时候再单独专⻔介绍,这⾥先提⼀下).

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Person
{
protected :
 string _name; // 姓名
 string _sex; // 性别
 int _age; // 年龄
};
class Student : public Person
{
public :
 int _No ; // 学号
};
int main()
{
 Student sobj ;
 // 1.派⽣类对象可以赋值给基类的指针/引⽤
 Person* pp = &sobj;
 Person& rp = sobj;
 
 // ⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的
 Person pobj = sobj;
 
 //2.基类对象不能赋值给派⽣类对象,这⾥会编译报错
 //sobj = pobj;
 
 return 0;
}

3.继承中的作⽤域

3.1隐藏规则

1️⃣在继承体系中基类和派⽣类都有独⽴的作⽤域.
2️⃣派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏.(在派⽣类成员函数中,可以使⽤基类::基类成员显示访问)
3️⃣需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏.
4️⃣注意在实际中在继承体系⾥⾯最好不要定义同名的成员.

cpp 复制代码
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是⾮常容易混淆
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Person
{
protected :
 string _name = "⼩李⼦"; // 姓名
 int _num = 111; // ⾝份证号
};
class Student : public Person
{
public:
 void Print()
 {
 cout<<" 姓名:"<<_name<< endl;
 cout<<" ⾝份证号:"<<Person::_num<< endl;
 cout<<" 学号:"<<_num<<endl;
 }
protected:
 int _num = 999; // 学号
};
int main()
{
 Student s1;
 s1.Print();
 
 return 0;
};

3.2继承作⽤域问题


4.派⽣类的默认成员函数

4.1 4个常⻅默认成员函数

6个默认成员函数,默认的意思就是指我们不写,编译器会变我们⾃动⽣成⼀个,那么在派⽣类中,这⼏个成员函数是如何⽣成的呢?
1️⃣派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员.如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显示调⽤.
2️⃣派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化.
3️⃣派⽣类的operator=必须要调⽤基类的operator=完成基类的复制.需要注意的是派⽣类的operator=隐藏了基类的operator=,所以显示调⽤基类的operator=,需要指定基类作⽤域.
4️⃣派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员.因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序.
5️⃣派⽣类对象初始化先调⽤基类构造再调派⽣类构造.
6️⃣派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构.
7️⃣因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个在多态部分会介绍).那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系.

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Person
{
public :
 Person(const char* name = "peter")
 : _name(name )
 {
 cout<<"Person()" <<endl;
 }
 
 Person(const Person& p)
 : _name(p._name)
 {
 cout<<"Person(const Person& p)" <<endl;
 }
 
 Person& operator=(const Person& p )
 {
 cout<<"Person operator=(const Person& p)"<< endl;
 if (this != &p)
 _name = p ._name;
 
 return *this ;
 }
 
 ~Person()
 {
 cout<<"~Person()" <<endl;
 }
protected :
 string _name ; // 姓名
};
class Student : public Person
{
   //默认成员函数 - 规则高度相似
	// 两份部分分开处理:
	// 1、基类成员(整体,调用基类构造)
 // 2、派生类成员(跟类和对象一样)
public :
 Student(const char* name, int num,const char* address)
 : Person(name)
 , _num(num )
 ,_address(address)
 {
 cout<<"Student()" <<endl;
 }
 
 Student(const Student& s)
 : Person(s)
 , _num(s ._num)
 ,_address(s._address)
 {
 cout<<"Student(const Student& s)" <<endl ;
 // 编译默认生成的就够用了
// 存在深拷贝时,才自己写
 }
 
 Student& operator = (const Student& s )
 {
 cout<<"Student& operator= (const Student& s)"<< endl;
 if (this != &s)
 {
 // 构成隐藏,所以需要显⽰调⽤
 Person::operator =(s);
 _num = s ._num;
 _address = s._address;
 }
 return *this ;
 } 

 ~Student()
 {
 cout<<"~Student()" <<endl;
 // Person::~Person();
 }// 自动调用父类析构, 才能保证先子后父的析构顺序
 // 派生类析构调用后,会自动调用父类析构,所以自己实现析构时不需要显示调用
// 构造初始化,先父类后子。析构清理资源,先子后父。
protected :
 int _num ; //学号
 string _address;
};
int main()
{
 Student s1 ("jack", 18,"北京");
 Student s2 (s1);
 Student s3 ("mark", 17,"上海");
 s1 = s3 ;
 return 0;
}

4.2实现⼀个不能被继承的类

⽅法1:基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以 后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象.
⽅法2:C++11新增了⼀个final关键字,final修改基类,派⽣类就不能继承了.

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
// C++11的⽅法
class Base final
{
public:
 void func5() { cout << "Base::func5" << endl; }
protected:
 int a = 1;
private:
 // C++98的⽅法
 /*Base()
 {}*/
};
class Derive :public Base 
{
 void func4() { cout << "Derive::func4" << endl; }
protected:
 int b = 2;
};
int main()
{
 Base b;
 Derive d;
 return 0;
}

5.继承与友元

友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员.

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Student;
class Person
{
public:
 friend void Display(const Person& p, const Student& s);
 protected:
 string _name; // 姓名
};
class Student : public Person
{
protected:
 int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
 cout << p._name << endl;
 cout << s._stuNum << endl;
}
int main()
{
 Person p;
 Student s;
 // 编译报错:error C2248: "Student::_stuNum": ⽆法访问 protected 成员
 // 解决⽅案:Display也变成Student 的友元即可
 Display(p, s);
 return 0;
}

6.继承与静态成员

基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员.⽆论派⽣出多少个派⽣类,都只有⼀个static成员实例.

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Person
{
public:
 string _name;
 static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
 int _stuNum;
 };
int main()
{
 Person p;
 Student s;
 // 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的
 // 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份
 cout << &p._name << endl;
 cout << &s._name << endl;
 // 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的
 // 说明派⽣类和基类共⽤同⼀份静态成员
 cout << &p._count << endl;
 cout << &s._count << endl;
 // 公有的情况下,⽗派⽣类指定类域都可以访问静态成员
 cout << Person::_count << endl;
 cout << Student::_count << endl;
 return 0;
}

7.多继承及其菱形继承问题

7.1继承模型

1️⃣单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承.
2️⃣多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯.
3️⃣菱形继承:菱形继承是多继承的⼀种特殊情况.菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份.⽀持多继承就⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我也是不建议设计出菱形继承这样的模型的.

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Person
{
public:
 string _name; // 姓名
};
class Student : public Person
{
protected:
 int _num; //学号
};
class Teacher : public Person
{
protected:
 int _id; // 职⼯编号
};
class Assistant : public Student, public Teacher
{
protected:
 string _majorCourse; // 主修课程
};
int main()
{
 // 编译报错:error C2385: 对"_name"的访问不明确
 Assistant a;
 a._name = "peter";
 // 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决
 a.Student::_name = "xxx";
 a.Teacher::_name = "yyy";
 return 0;
}

7.2虚继承

很多⼈说C++语法复杂,其实多继承就是⼀个体现.有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有⼀些损失,所以最好不要设计出菱形继承.多继承可以认为是C++的缺陷之⼀,后来的⼀些编程语⾔都没有多继承,如Java.

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Person
{
public:
 string _name; // 姓名
 /*int _tel;
 int _age;
 string _gender;
 string _address;*/
 // ...
};
//使⽤虚继承Person类
class Student : virtual public Person
{
protected:
 int _num; //学号
};
// 使⽤虚继承Person类
class Teacher : virtual public Person
{
protected:
 int _id; // 职⼯编号
};
// 教授助理
class Assistant : public Student, public Teacher
{
protected:
 string _majorCourse; // 主修课程
};
//数据冗余 二义性 ---virtual继承
int main()
{
//编译报错:error C2385: 对"_name"的访问不明确
//使⽤虚继承,可以解决数据冗余和⼆义性
 Assistant a;
 a._name = "peter";
//需要显示指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决
//a.Student::_name = "xxx";
//a.Teacher::_name = "yyy";
 return 0;
}

我们可以设计出多继承,但是不建议设计出菱形继承,因为菱形虚拟继承以后,⽆论是使⽤还是底层都会复杂很多.当然有多继承语法⽀持,就⼀定存在会设计出菱形继承,像Java是不⽀持多继承的,就避开了菱形继承.

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Person
{
public:
 Person(const char* name)
 :_name(name)
 {}
 string _name; // 姓名
};
class Student : virtual public Person
{
public:
 Student(const char* name, int num)
 :Person(name)
 ,_num(num)
 {}
protected:
 int _num; //学号
};
class Teacher : virtual public Person
{
public:
 Teacher(const char* name, int id)
 :Person(name)
 , _id(id)
 {}
protected:
 int _id; // 职⼯编号
};
// 不要去玩菱形继承
class Assistant : public Student, public Teacher
{
public:
 Assistant(const char* name1, const char* name2, const char* name3)
 :Person(name3)
 ,Student(name1, 1)
 ,Teacher(name2, 2)
 {}
protected:
 string _majorCourse; // 主修课程
};
int main()
{
 // 思考⼀下这⾥a对象中_name是"张三", "李四", "王五"中的哪⼀个?
 Assistant a("张三", "李四", "王五");
 return 0;
}

7.3多继承中指针偏移问题


7.4IO库中的菱形虚拟继承


实际中不建议设计出菱形继承及菱形虚拟继承,⼀⽅⾯太复杂容易出问题,另⼀⽅⾯这样的模型,访问基类成员有⼀定得性能损耗.所以菱形继承、菱形虚拟继承的虚表就不介绍了,⼀般也不需要研究清楚,因为实际中很少⽤.如果感兴趣的,可以去看下⾯的两篇链接⽂章.
C++ 虚函数表解析
C++ 对象的内存布局

cpp 复制代码
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_ostream : virtual public std::basic_ios<CharT, Traits>
{};
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_istream : virtual public std::basic_ios<CharT, Traits>
{};

8.继承和组合

1️⃣public继承是⼀种is-a的关系.也就是说每个派⽣类对象都是⼀个基类对象.
2️⃣组合是⼀种has-a的关系.假设B组合了A,每个B对象中都有⼀个A对象.
3️⃣继承允许你根据基类的实现来定义派⽣类的实现.这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤(white-box reuse).术语"⽩箱"是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对派⽣类可⻅.继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响.派⽣类和基类间的依赖关系很强,耦合度⾼.
4️⃣对象组合是类继承之外的另⼀种复⽤选择.新的更复杂的功能可以通过组装或组合对象来获得.对象组合要求被组合的对象具有良好定义的接⼝.这种复⽤⻛格被称为⿊箱复⽤(black-box reuse),因为对象的内部细节是不可⻅的.对象只以"⿊箱"的形式出现. 组合类之间没有很强的依赖关系,耦合度低.优先使⽤对象组合有助于你保持每个类被封装.
5️⃣优先使⽤组合,⽽不是继承.实际尽量多去⽤组合,组合的耦合度低,代码维护性好.不过也不太那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承.类之间的关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合.

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
using namespace std;
// Tire(轮胎)和Car(⻋)更符合has-a的关系
class Tire {
protected:
 string _brand = "Michelin"; // 品牌
 size_t _size = 17; // 尺⼨
};
class Car {
protected:
 string _colour = "⽩⾊"; // 颜⾊
 string _num = "陕ABIT00"; // ⻋牌号
 Tire _t1; // 轮胎
 Tire _t2; // 轮胎
 Tire _t3; // 轮胎
 Tire _t4; // 轮胎
};
class BMW : public Car {
public:
 void Drive() { cout << "好开-操控" << endl; }
};
// Car和BMW/Benz更符合is-a的关系
class Benz : public Car {
public:
 void Drive() { cout << "好坐-舒适" << endl; }
};
template<class T>
class vector
{};
// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public vector<T>
{};
template<class T>
class stack
{
public:
 vector<T> _v;
};
int main()
{
 return 0;
}

9.菱形虚拟继承原理剖析

继承中我介绍C++的多继承就会引发⼀些场景出现菱形继承,有了菱形继承,就会出现数据冗余和⼆义性的问题,C++⼜引⼊了虚继承来解决数据冗余和⼆义性.

cpp 复制代码
class Person
{
public :
 string _name ; // 姓名
};
// class Student : public Person
class Student : virtual public Person
{
protected :
 int _num ; // 学号
};
// class Teacher : public Person
class Teacher : virtual public Person
{
protected :
 int _id ; // 职⼯编号
};
class Assistant : public Student, public Teacher
{
protected :
 string _majorCourse ; // 主修课程
};
void Test ()
{
 // 这样会有⼆义性⽆法明确知道访问的是哪⼀个
 Assistant a ;
 a._name = "peter";
 // 需要显⽰指定访问哪个⽗类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决
 a.Student::_name = "xxx";
 a.Teacher::_name = "yyy";
 }


1️⃣为了研究虚拟继承原理,我们给出了⼀个简化的菱形继承体系,再借助内存窗⼝观察对象成员的模型.要注意的是这⾥必须借助内存窗⼝才能看到真实的底层对象内存模型,vs编译器的监视窗⼝是经过特殊处理的,以它的⻆度给出了⼀个⽅便看的样⼦,但并不是本来的样⼦.但是有时想看清真实内存模型,往往需要借助内存窗⼝.
2️⃣通过下⾯的简化菱形虚拟继承模型,我们可以看到,D对象中的B和C部分中分别包含⼀个指向指向虚基表,B指向的虚基表中存储了B对象部分距离公共的A的相对偏移量距离,C指向的虚基表中存储了C对象部分距离公共的A的相对偏移量距离.这样公共的虚基类A部分在D对象中就只有⼀份了,这样就解决了数据冗余和⼆义性的问题.
3️⃣通过B的对象模型,我们发现菱形虚拟继承中B和C的对象模型跟D保持的⼀致的⽅式去存储管理A,这样当B的这指针访问A时,⽆论B指针切⽚指向D对象,还是B指针直接指向B对象,访问A成员都是通过虚基表指针的⽅式查找到A成员再访问.

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
 virtual void func1() {}
public:
	int _a;
};
// class B : public A
class B : virtual public A
{
public:
 virtual void func1() {}
 virtual void func2() {}
public:
	int _b;
};
// class C : public A
class C : virtual public A
{
public:
 virtual void func1() {}
 virtual void func3() {}
public:
	int _c;
};
class D : public B, public C
{
public:
 D()
 :_d(1)
 {
 }
 inline virtual void func1() {}
 virtual void func4() {}
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._a = 3;
	d._b = 4;
	d._c = 5;
	d._d = 6;
	return 0;
}
cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	int _a;
};
// class B : public A
class B : virtual public A
{
public:
	int _b;
};
// class C : public A
class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};
int main()
{
 D d;
 d.B::_a = 1;
 d.C::_a = 2;
 d._a = 3;
 d._b = 4;
 d._c = 5;
 d._d = 6;
 B b;
 b._a = 7;
 b._b = 8;
 // B的指针指向B对象
 B* p2 = &b;
 // B的指针指向D对象切⽚
 B* p1 = &d;
 //p1和p2分别对指向的_a成员访问修改
 //分析内存模型,我们发现B对象也使⽤了虚基表指向A成员的模型
 //所以打开汇编我们看到下⾯的访问_a的⽅式是⼀样的
 p1->_a++;
 p2->_a++;
 return 0;
}

10.单继承虚函数表深⼊探索

vs编译器的监视窗⼝是经过特殊处理的,以它的⻆度给出了⼀个⽅便看的样⼦,但并不是本来的样⼦.在多态部分我会介绍虚函数指针都要放进虚函数表,这⾥我们通过监视窗⼝观察Derive对象,看不到func3和func4在虚表中,借助内存窗⼝可以看到⼀个地址,但是并不确认是不是func3和func4的地址.所以下⾯我们写了⼀份特殊代码,通过指针的⽅式,强制访问了虚函数表,调⽤了虚函数,确认继承中虚函数表中的真实内容.

cpp 复制代码
#include<iostream>
using namespace std;
class Base {
public:
 virtual void func1() { cout << "Base::func1" << endl; }
 virtual void func2() { cout << "Base::func2" << endl; }
private:
 int a;
};
class Derive :public Base {
public:
 virtual void func1() { cout << "Derive::func1" << endl; }
 virtual void func3() { cout << "Derive::func3" << endl; }
 virtual void func4() { cout << "Derive::func4" << endl; }
 void func5() { cout << "Derive::func5" << endl; }
private:
 int b;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
 // 依次取虚表中的虚函数指针打印并调⽤。调⽤就可以看出存的是哪个函数
 cout << " 虚表地址>" << vTable << endl;
 // 注意如果是在g++下⾯,这⾥就不能⽤nullptr去判断访问虚表结束了
 for (int i = 0; vTable[i] != nullptr; ++i)
 {
 printf(" 第%d个虚函数地址 :0X%p,->", i, vTable[i]);
 VFPTR f = vTable[i];
 f();
 }
 cout << endl;
}
int main()
{
 Base b;
 Derive d;
 // 32位程序的访问思路如下:
 // 需要注意的是如果是在64位下,指针是8byte,对应程序位置就需要进⾏更改
 // 思路:取出b、d对象的头4bytes,就是虚表的指针,前⾯我们说了虚函数表本质是⼀个存虚
//函数指针的指针数组,vs下这个数组最后⾯放了⼀个nullptr,g++下⾯最后没有nullptr
 // 1.先取b的地址,强转成⼀个int*的指针
 // 2.再解引⽤取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
 // 3.再强转成VFPTR*,因为虚表就是⼀个存VFPTR类型(虚函数指针类型)的数组。
 // 4.虚表指针传递给PrintVTable进⾏打印虚表
 // 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不⼲净,虚
//表最后⾯没有放nullptr,导致越界,这是编译器的问题。我们只需要点⽬录栏的-⽣成-清理解决⽅
//案,再编译就好了。
 VFPTR* vTable1 = (VFPTR*)(*(int*)&b);
 PrintVTable(vTable1);
 VFPTR* vTable2 = (VFPTR*)(*(int*)&d);
 PrintVTable(vTable2);
 return 0;
}

11.多继承虚函数表深⼊探索

1️⃣跟前⾯单继承类似,多继承时Derive对象的虚表在监视窗⼝也观察不到部分虚函数的指针.所以我们⼀样可以借助上⾯的思路强制打印虚函数表.
2️⃣需要注意的是多继承时,Derive中同时继承了Base1和Base2,内存中先继承的对象在前⾯,并且Derive中包含的Base1和Base2各有⼀张虚函数表,通过观察我们发现Derive没有重写的虚函数func3,选择放在先继承的Base1的虚函数表中.
3️⃣另外需要注意的是,Derive对象中重写的Base1虚表的func1地址和重写Base2虚表的func1地址不⼀样,这是为什么呢?这个问题还⽐较复杂.需要我们分别对这两个函数进⾏多态调⽤,并翻阅对应的汇编代码进⾏分析,才能捋清楚问题所在.这⾥简单说⼀个结论就是本质Base2虚表中func1的地址并不是真实的func1的地址,⽽是封装过的func1地址,因为Base2指针p2指向Derive时,Base2部分在中间位置,切⽚时,指针会发⽣偏移,那么多态调⽤p2->func1()时,p2传递给this前需要把p2给修正回去指向Derive对象,因为func1是Derive重写的,⾥⾯this应该是指向Derive对象的.


cpp 复制代码
#include<iostream>
using namespace std;
class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%p,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	/*Base2* ptr = &d;
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((ptr)));*/ 
	PrintVTable(vTableb2);
	Base1* p1 = &d;
	p1->func1();
	Base2* p2 = &d;
	p2->func1();
	d.func1();
	return 0;
}

敬请期待下一篇文章内容-->C++多态相关内容!


每日心灵鸡汤:
二十二岁,天地空无,嗔痴如常.而我却见己明心,知玉可琢而成之、人亦如此,于是我行止落拓,不强求、不执着,任尘寰世相纷纭、千山万水,我只固守本心,竭力前行.我想我的命轨里依旧蛰伏着不灭的少年心气,但我思泊如,一切于我眼前铺陈,我只沉稳静待,从容行抵.
生命或如一场雪零落在冬季,摇摇不定,终生都在寻找一处供以栖息的山脊.然而我自成山,甘愿负起自我一生的生死爱恨与聚散盈缺,永坚韧、自挺拔,恒伫立.千千万万次,找到自己,绝不迟疑.

相关推荐
hamish-wu2 小时前
告别idea,拥抱AI开发环境TRAE
java·ide·编辑器·intellij-idea·intellij idea·visual studio
天空属于哈夫克32 小时前
Go 开发:企微外部群主动发送消息
开发语言·golang·企业微信
GeminiJM2 小时前
LangGraph 源码学习笔记
linux·笔记·学习·langchain
charlie1145141912 小时前
RK3568跑Ubuntu 24.04全路程指南(以正点原子的RK3568开发板为例子)
linux·笔记·ubuntu·rootfs·教程·环境配置·rk3568
babe小鑫2 小时前
大专应用统计学专业学习数据分析的实用性分析
学习·数据挖掘·数据分析
简佐义的博客2 小时前
15万单细胞、19种实体瘤:系统学习血管内皮细胞泛癌的单细胞与空间转录组联合分析思路
人工智能·学习
小龙2 小时前
【学习笔记】视频抽帧方法大全
笔记·学习·计算机视觉·视频·视频抽帧
会周易的程序员2 小时前
openplc runtime v4 安全
网络·c++·物联网·websocket·安全·https·ssl
不绝1912 小时前
延迟函数/协同程序
java·开发语言