目录
[2. 虚继承](#2. 虚继承)
[1. public 继承(is - a 关系)](#1. public 继承(is - a 关系))
[2. 组合(has - a 关系)](#2. 组合(has - a 关系))
[3. 应用建议](#3. 应用建议)
一、继承的概念及定义
1.继承的概念
继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有 类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。继承 呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的 复⽤,继承是类设计层次的复⽤。
下⾯我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/ 电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类⾥⾯就是冗余的。当然他们 也有⼀些不同的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣ 的独有成员函数是学习,⽼师的独有成员函数是授课。
cpp
class Student {
public:
// 进入校园、图书馆、实验室刷二维码等身份认证
void identity() {
//...
}
// 学习
void study() {
//...
}
protected:
std::string _name = "peter"; // 姓名
std::string _address; // 地址
std::string _tel; // 电话
int _age = 18; // 年龄
int _stuid; // 学号
};
class Teacher {
public:
// 进入校园、图书馆、实验室刷二维码等身份认证
void identity() {
//...
}
// 授课
void teaching() {
//...
}
protected:
std::string _name = "张三"; // 姓名
int _age = 18; // 年龄
std::string _address; // 地址
std::string _tel; // 电话
std::string _title; // 职称
};
下⾯我们公共的成员都放到Person类中,Student和teacher都继承Person,就可以复⽤这些成员,就 不需要重复定义了,省去了很多⿇烦。
cpp
//父类
class Person {
public:
// 进入校园、图书馆、实验室刷二维码等身份认证
void identity() {
std::cout << "void identity()" << _name << std::endl;
}
protected:
std::string _name = "张三"; // 姓名
std::string _address; // 地址
std::string _tel; // 电话
int _age = 18; // 年龄
};
//子类
class Student : public Person {
public:
// 学习
void study() {
//...
}
protected:
int _stuid; // 学号
};
//子类
class Teacher : public Person {
public:
// 授课
void teaching() {
//...
}
protected:
std::string title;
};
int main() {
Student s;
Teacher t;
s.identity();
t.identity();
return 0;
}
2.继承的定义
定义格式
下⾯我们看到Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。(因为翻译的原因,所以 既叫基类/派⽣类,也叫⽗类/⼦类)
继承方式和访问限定符
继承基类成员访问⽅式的变化
基类当中被不同访问限定符修饰的成员,以不同的继承方式继承到派生类当中后,该成员最终在派生类当中的访问方式将会发生变化。
|----------------|-----------------|-----------------|---------------|
| 类成员/继承方式 | public继承 | protected继承 | private继承 |
| 基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
- 基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员虽然被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问 它。
2.基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3.对上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。基类的其他成员 在派⽣类的访问⽅式==Min(成员在基类的访问限定符,继承⽅式),public >protected> private。
4.使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显 ⽰的写出继承⽅式。
5.在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤ protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实 际中扩展维护性不强。
cpp
#include <iostream>
#include <string>
// 基类 Person
class Person {
public:
void Print() {
std::cout << _name << std::endl;
}
protected:
std::string _name = "Unknown";
private:
int _age = 18;
};
// 以 public 继承方式的 Student 类
class Student : public Person {
protected:
int _stunum;
};
// 以 protected 继承方式的 Student 类
// class Student : protected Person {
// protected:
// int _stunum;
// };
// 以 private 继承方式的 Student 类
// class Student : private Person {
// protected:
// int _stunum;
// };
int main() {
Student s;
s._name = "Alice";//报错,类外不能直接访问protect成员
s.Print();
// 以下代码用于演示不同继承方式下访问权限的变化
// 如果是 protected 继承,下面这行在 main 中会报错,因为 _name 变成 protected 了
// 如果是 private 继承,下面这行在 main 中会报错,因为 _name 变成 private 了
// s._name = "Bob";
return 0;
}
3.继承类模板
使用样例
cpp
#include <iostream>
// 定义一个名为Base的类模板,类型参数为T
template <typename T>
class Base {
public:
// 构造函数,用于初始化Base类的成员变量baseValue
Base(T value)
: baseValue(value)
{}
// 获取baseValue值的成员函数,该函数不会修改类的成员变量
T getBaseValue() const
{
return baseValue;
}
protected:
// 受保护的成员变量,用于存储Base类的数据
T baseValue;
};
// 定义一个名为Derived的类模板,继承自Base<T>
template <typename T>
class Derived : public Base<T> {
public:
// 构造函数,用于初始化Derived类的成员变量
// 其中先调用基类Base<T>的构造函数初始化从基类继承来的部分
// 再初始化自身的derivedValue成员变量
Derived(T value1, T value2)
: Base<T>(value1),
derivedValue(value2)
{}
// 获取derivedValue值的成员函数,该函数不会修改类的成员变量
T getDerivedValue() const
{
return derivedValue;
}
private:
// 受保护的成员变量,用于存储Derived类特有的数据
T derivedValue;
};
int main() {
// 实例化Derived类模板,将类型参数T指定为int
// 创建一个Derived<int>类型的对象d,并传入两个int值作为构造函数参数
Derived<int> d(10, 20);
// 调用从Base<int>类继承来的getBaseValue函数,输出基类中的值
std::cout << "Base value: " << d.getBaseValue() << std::endl;
// 调用Derived类自身的getDerivedValue函数,输出派生类中的值
std::cout << "Derived value: " << d.getDerivedValue() << std::endl;
return 0;
}
注意事项样例:
cpp
//继承类模板
namespace bit {
// 定义一个栈类模板,继承自标准库中的 vector 类模板
// 这里 stack 和 vector 的关系,既符合 is-a(继承关系,表示栈是一种特殊的向量),也符合 has-a(栈内部包含了 vector 的功能实现)
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);
}
// 弹出栈顶元素的函数
void pop() {
vector<T>::pop_back();
}
// 获取栈顶元素的函数(返回栈顶元素的常引用)
const T& top() {
return vector<T>::back();
}
// 判断栈是否为空的函数
bool empty() {
return vector<T>::empty();
}
};
} // namespace bit
int main() {
bit::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty()) {
std::cout << st.top() << " ";
st.pop();
}
return 0;
}
二、基类和派生类对象赋值转换
1.public继承的派⽣类对象可以赋值给基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切 割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。
2.基类对象不能赋值给派⽣类对象,派生类对象通常比基类对象占用更多内存空间,因为它不仅包含从基类继承来的所有成员,还额外拥有自身定义的成员。C++ 是强类型语言,类型之间的转换需要遵循严格规则。基类和派生类是不同的类型,派生类是基类的 "特殊形式",但反之不成立
3.基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针 是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-TimeType Information)的dynamic_cast 来进⾏识别后进⾏安全转换。(ps:这个我们后⾯类型转换章节再 单独专⻔讲解,这⾥先提⼀下)
代码示例:
cpp
class Person {
protected:
std::string _name; // 姓名
std::string _sex; // 性别
int _age; // 年龄
};
// 定义派生类Student,继承自Person类
class Student : public Person {
public:
int _No; // 学号
};
int main() {
Student sobj;
// 1. 派生类对象可以赋值给基类的指针或引用
Person* pp = &sobj;
Person& rp = sobj;
Person pobj = sobj;
// 派生类对象可以赋值给基类的对象是通过调用后面会讲解的基类的拷贝构造函数完成的
// 2. 基类对象不能赋值给派生类对象,这里会编译报错
// sobj = pobj; // 取消注释此行代码会导致编译错误,因为基类对象所包含的数据成员可能比派生类少,
// 这样的赋值可能会导致派生类中特有成员(如Student类中的_No)没有被正确初始化,不符合逻辑,所以编译器不允许这样的操作。
return 0;
}
可以观察到这里的pp指针只会指向父类的成员变量。像一个切片,把派⽣类中基类那部分切出来。
三、继承中的作用域
1.隐藏规则
• 在继承体系中基类和派⽣类都有独⽴的作⽤域。
• 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。 (在派⽣类成员函数中,可以使⽤基类::基类成员显⽰访问)
• 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
• 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
cpp
class Person {
public:
void func() {
std::cout << "Person:func" << std::endl;
}
protected:
int _num = 111;
std::string _name = "小李子";
};
// Student类继承自Person类,代表学生,除了继承自Person的属性外,还有自己的学号属性
class Student : public Person {
public:
void Print() {
std::cout << "姓名: " << _name << std::endl;
std::cout << "身份证号: " << Person::_num << std::endl;
std::cout << "学号: " << _num << std::endl;
}
void func() {
std::cout << "Student:func" << std::endl;
}
protected:
int _num = 999;
};
int main() {
Student s1;
s1.Print();
s1.func();
s1.Person::func();//调用基类当中的func需指定作用域
return 0;
}
特别注意: 代码当中,父类中的func和子类中的func不是构成函数重载,因为函数重载要求两个函数在同一作用域,而此时这两个func函数并不在同一作用域。为了避免类似问题,实际在继承体系当中最好不要定义同名的成员。
四、派⽣类的默认成员函数
默认成员函数,即我们不写编译器会自动生成的函数,类当中的默认成员函数有以下六个:
代码解析:
cpp
#include <iostream>
#include <string>
// 定义Person类,作为基类
class Person {
public:
// 构造函数,用于初始化Person对象
// 参数name有默认值"peter",如果调用构造函数时不传参,就使用该默认值
Person(const char* name = "peter")
: _name(name) { // 使用初始化列表初始化_name成员变量
std::cout << "Person()" << std::endl;
}
// 拷贝构造函数,用于从另一个Person对象创建当前对象
// 拷贝构造函数接收一个const引用,防止修改传入的对象,同时避免不必要的拷贝开销
Person(const Person& p)
: _name(p._name) { // 从传入对象p拷贝_name成员变量的值
std::cout << "Person(const Person& p)" << std::endl;
}
// 赋值运算符重载,用于将一个Person对象赋值给当前对象
Person& operator=(const Person& p) {
std::cout << "Person operator=(const Person& p)" << std::endl;
if (this!= &p) { // 防止自我赋值
_name = p._name; // 将传入对象p的_name赋值给当前对象的_name
}
return *this;
}
// 析构函数,用于清理Person对象占用的资源
~Person() {
std::cout << "~Person()" << std::endl;
}
protected:
std::string _name; // 姓名,声明为protected,方便子类访问
};
// 定义Student类,继承自Person类
class Student : public Person {
public:
// 构造函数,用于初始化Student对象
// 参数num用于初始化_num,name用于初始化从Person类继承来的_name
Student(int num, const char* name)
: _num(num),
Person(name) { // 先调用基类构造函数初始化继承自基类的部分,必须放在初始化列表最前面
}
// 拷贝构造函数,用于从另一个Student对象创建当前对象
// 先调用基类的拷贝构造函数初始化继承自基类的部分,再初始化自身成员变量_num
Student(const Student& s)
: Person(s),
_num(s._num) {
std::cout << "Student(const Student& s)" << std::endl;
}
// 赋值运算符重载,用于将一个Student对象赋值给当前对象
Student& operator = (const Student& s) {
std::cout << "Student& operator= (const Student& s)" << std::endl;
if (this!= &s) {
_num = s._num; // 先更新自身成员变量_num
Person::operator=(s); // 调用基类的赋值运算符重载,处理继承自基类的部分
}
return *this;
}
// 析构函数,用于清理Student对象占用的资源
~Student() {
std::cout << "~Student()" << std::endl;
// 这里不需要显式调用Person::~Person();,因为子类析构函数会自动调用基类析构函数
}
protected:
int _num; // 学号,声明为protected,方便子类访问
};
// 特点:子类继承下来的父类成员当做一个整体对象
// 构造:
// 默认:子类成员 内置类型(有缺省就用,没有给随机值)和自定义类型(默认构造)+父类成员(必须用父类的默认构造函数)
// 解释:当创建子类对象时,如果没有显式初始化子类的内置类型成员,有默认值就用默认值,没有则随机初始化;
// 对于自定义类型成员会调用其默认构造函数。同时一定会调用父类的默认构造函数来初始化父类部分。
// 拷贝构造:子类成员 内置类型(值拷贝)和自定义类型(这个类型拷贝构造)+父类构造(必须调用父亲拷贝构造)
// 解释:拷贝构造子类对象时,内置类型成员直接进行值拷贝,自定义类型成员会调用它自身的拷贝构造函数,
// 并且必须调用父类的拷贝构造函数来正确拷贝父类部分。
// 析构:自己实现:不需要显示调用父类析构,子类析构结束后,会自动调用父类。
// 解释:在子类析构函数里,不需要手动去调用父类的析构函数,因为C++机制保证在子类析构函数执行完毕后,
// 会自动触发父类析构函数,按顺序清理对象资源。
int main() {
// 创建Student对象s1,调用Student构造函数,先触发Person构造函数
Student s1(13, "张三");
// 调用Student的拷贝构造函数创建s2,先触发Person的拷贝构造函数
Student s2 = s1;
// 创建Student对象s3,调用Student构造函数,先触发Person构造函数
Student s3(12, "李四");
// 调用Student的赋值运算符重载函数,先处理自身成员赋值,再调用Person的赋值运算符
s3 = s2;
return 0;
}
默认成员函数总结:
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的 operator = 必须要调用基类的 operator = 完成基类的复制。需要注意的是派生类的 operator = 隐藏了基类的 operator=,所以显示调用基类的 operator=,需要指定基类作用域。在派生类的拷贝构造函数和operator=当中调用基类的拷贝构造函数和operator=的传参方式是一个切片行为,都是将派生类对象直接赋值给基类的引用。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象初始化先调用基类构造再调派生类构造。
- 派生类对象析构清理先调用派生类析构再调基类的析构。
- 因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同 (这个我们多态章节会讲)。那么编译器会对析构函数名进行特殊处理,处理成 destructor (),所以基类析构函数不加 virtual 的情况下,派生类析构函数和基类析构函数构成隐藏关系。
五、继承与友元
友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员 。
例如,以下代码中Display函数是基类Person的友元,当时Display函数不是派生类Student的友元,即Display函数无法访问派生类Student当中的私有和保护成员。
cpp
#include <iostream>
#include <string>
using namespace std;
class Student;
class Person
{
public:
//声明Display是Person的友元
friend void Display(const Person& p, const Student& s);
protected:
string _name; //姓名
};
class Student : public Person
{
protected:
int _id; //学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl; //可以访问
cout << s._id << endl; //无法访问
}
int main()
{
Person p;
Student s;
Display(p, s);
return 0;
}
若想让Display函数也能够访问派生类Student的私有和保护成员,只能在派生类Student当中进行友元声明。
cpp
class Student : public Person
{
public:
//声明Display是Student的友元
friend void Display(const Person& p, const Student& s);
protected:
int _id; //学号
};
六、继承与静态成员
若基类当中定义了一个static静态成员变量,则在整个继承体系里面只有一个该静态成员。无论派生出多少个子类,都只有一个static成员实例。
cpp
#include <iostream>
#include <string>
// 定义Person类,代表一般的人员信息
class Person {
public:
std::string _name; // 人员的姓名,非静态成员变量,每个类的对象都有自己独立的一份
static int _count; // 静态成员变量,被所有该类以及派生类的对象所共享,用来记录一些和类相关的计数等情况
};
// 在类外初始化静态成员变量_count,初始值设为0
int Person::_count = 0;
// Student类继承自Person类,代表学生,除了继承Person类的成员外,还有自己的学号属性
class Student : public Person {
protected:
int _stuNum; // 学生的学号
};
int main() {
Person p;
Student s;
// 输出非静态成员_name的地址,会发现它们不一样,因为非静态成员在每个对象中都有独立的存储
std::cout << &p._name << std::endl;
std::cout << &s._name << std::endl;
// 输出静态成员_count的地址,会发现它们是一样的,这表明静态成员在整个类及其派生类体系中只有一份存储,被所有对象共享
std::cout << &p._count << std::endl;
std::cout << &s._count << std::endl;
Person::_count++;
// 在公有继承的情况下,无论是通过基类还是派生类指定类域,都可以访问静态成员
std::cout << Person::_count << std::endl;
std::cout << Student::_count << std::endl;
return 0;
}
输出:
cpp
0000003452DAF6B8
0000003452DAF6F8
00007FF7087344C4
00007FF7087344C4
1
1
七、多继承及其菱形继承问题
1.继承模型
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型 是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。
菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以 看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就 ⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。
代码示例:
cpp
//菱形继承
#include <iostream>
#include <string>
// 定义Person类,代表一般的人员信息,包含姓名属性
class Person {
public:
std::string _name; // 姓名
};
class Student : public Person {
protected:
int _num; // 学号
};
class Teacher : public Person {
protected:
int _id; // 职工编号
};
// Assistant类继承自Student和Teacher类,增加主修课程属性,可用于表示助教相关信息
class Assistant : public Student, public Teacher {
protected:
std::string _majorCourse; // 主修课程
};
int main() {
Assistant a;
a._name = "peter";
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
报错信息:
Assistant对象是多继承的Student和Teacher,而Student和Teacher当中都继承了Person,因此Student和Teacher当中都有_name成员,若是直接访问Assistant对象的_name成员会出现访问不明确的报错。
对于此,我们可以显示指定访问Assistant哪个父类的_name成员。
cpp
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
但内存中Assistant
类的对象布局里,仍然实实在在地保存着两份_name
数据。上述代码只是明确告知编译器我们要访问哪一条继承路径下的_name
,以此解决访问的二义性问题,但并没有改变对象内部存储了两份相同数据的事实。
2. 虚继承
为了解决菱形继承的二义性和数据冗余问题,出现了虚拟继承(virtrual关键字)。如前面说到的菱形继承关系,在Student和Teacher继承Person是使用虚拟继承,即可解决问题。
虚继承代码如下
cpp
//菱形继承
#include <iostream>
#include <string>
// 定义Person类,代表一般的人员信息,包含姓名属性
class Person {
public:
std::string _name; // 姓名
};
// Student类虚继承自Person类,增加学号属性,用于表示学生相关信息
class Student :virtual public Person {
protected:
int _num; // 学号
};
// Teacher类虚继承自Person类,增加职工编号属性,用于表示教师相关信息
class Teacher :virtual public Person {
protected:
int _id; // 职工编号
};
// Assistant类继承自Student和Teacher类,增加主修课程属性,可用于表示助教相关信息
class Assistant : public Student, public Teacher {
protected:
std::string _majorCourse; // 主修课程
};
int main() {
Assistant a;
a._name = "peter";
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
此时就可以直接访问Assistant对象的_name成员了,并且之后就算我们指定访问Assistant的Student父类和Teacher父类的_name成员,访问到的都是同一个结果,解决了二义性的问题。
cpp
cout << a.Student::_name << endl; //peter
cout << a.Teacher::_name << endl; //peter
而我们打印Assistant的Student父类和Teacher父类的_name成员的地址时,显示的也是同一个地址,解决了数据冗余的问题。
cpp
cout << &a.Student::_name << endl; //0136F74C
cout << &a.Teacher::_name << endl; //0136F74C
3.菱形虚拟继承原理
在此之前,我们先看看不使用菱形虚拟继承时,以下菱形继承当中D类对象的各个成员在内存当中的分布情况。
示例代码:
cpp
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
string _name; //姓名
};
class Student : virtual public Person //虚拟继承
{
protected:
int _num; //学号
};
class Teacher : virtual public Person //虚拟继承
{
protected:
int _id; //职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; //主修课程
};
int main()
{
Assistant a;
a._name = "peter"; //无二义性
return 0;
}
也就是说,D类对象当中各个成员在内存当中的分布情况如下:
现在我们再来看看使用菱形虚拟继承时,以下菱形继承当中D类对象的各个成员在内存当中的分布情况。
代码如下:
cpp
#include <iostream>
using namespace std;
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
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._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
通过内存窗口,我们可以看到D类对象当中各个成员在内存当中的分布情况如下:
其中D类对象当中的_a成员被放到了最后,而在原来存放两个_a成员的位置变成了两个指针,这两个指针叫虚基表指针,它们分别指向一个虚基表。
虚基表中包含两个数据,第一个数据是为多态的虚表预留的存偏移量的位置(这里我们不必关心),第二个数据就是当前类对象位置距离公共虚基类的偏移量。
也就是说,这两个指针经过一系列的计算,计算偏移量的方式已经写在图中,最终都可以找到成员_a。
我们若是将D类对象赋值给B类对象,在这个切片过程中,就需要通过虚基表中的第二个数据找到公共虚基类A的成员,得到切片后该B类对象在内存中仍然保持这种分布情况。
cpp
D d;
B b = d; //切片行为
得到切片后该B类对象当中各个成员在内存当中的分布情况如下
其中,_a对象仍然存储在该B类对象的最后。
八、继承和组合
1. public 继承(is - a 关系)
- 定义:public 继承是一种 is - a 的关系,意味着每个派生类对象都是一个基类对象。
- 原理 :
- 通过继承,派生类继承了基类的所有非私有成员(属性和方法)。
- 在这种关系中,基类的内部细节对派生类是可见的,这种复用方式被称为白箱复用(white - box reuse)。
- 派生类和基类间的依赖关系很强,耦合度高。继承在一定程度上破坏了基类的封装,因为基类的改变会对派生类产生很大的影响。
2. 组合(has - a 关系)
- 定义:组合是一种 has - a 的关系,假设 B 组合了 A,每个 B 对象中都有一个 A 对象。
- 原理 :
- 新的、更复杂的功能可以通过组装或组合对象来获得。
- 对象组合要求被组合的对象具有良好定义的接口,这种复用风格被称为黑箱复用(black - box reuse),因为对象的内部细节是不可见的,对象只以 "黑箱" 的形式出现。
- 组合类之间没有很强的依赖关系,耦合度低,优先使用对象组合有助于保持每个类被封装。
3. 应用建议
- 优先使用组合:实际中尽量多去用组合,组合的耦合度低,代码维护性好。
- 有条件地使用继承:不过也不那么绝对,类之间的关系适合继承(is - a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is - a)也适合组合(has - a),就用组合。
代码示例:
cpp
// 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;
}
总结来说,继承和组合是面向对象编程中实现代码复用的两种主要方式,各有优缺点,在实际编程中需要根据具体情况合理选择。
本篇博客到此结束,如有问题,欢迎评论区留言~