跳到主要内容C++ 继承机制详解 | 极客日志C++
C++ 继承机制详解
综述由AI生成系统讲解了 C++ 继承的核心机制。内容包括继承的基本概念与定义、成员访问控制规则(public/protected/private)、继承类模板的使用、基类与派生类间的转换规则(切片与向下转型)。详细阐述了继承中的作用域与隐藏规则、派生类默认成员函数(构造、拷贝、赋值、析构)的生成与调用顺序。此外,还探讨了友元关系的不可继承性、静态成员的共享特性、多继承模型及菱形继承问题的解决方案(虚继承)。最后对比了继承与组合的区别,强调根据 is-a 或 has-a 关系选择合适的复用方式。
GRACE Grace24 浏览 1. 继承的概念及定义
1.1 继承的概念
继承(inheritance)是面向对象程序设计中代码复用的重要手段,允许在保持原有类特性的基础上进行扩展,产生新的类(派生类/子类)。它体现了面向对象的层次结构,实现了类设计层次的复用。
示例场景:
- 未使用继承时,Student 和 Teacher 类中存在大量冗余代码(如姓名、地址、电话、年龄等成员变量,以及身份认证 identity() 成员函数)。
- 使用继承后,可将公共成员提取到 Person 基类中,Student 和 Teacher 作为派生类继承 Person,避免重复定义。
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;
};
class {
:
{
cout << << _name << endl;
}
:
string _name = ;
string _address;
string _tel;
_age = ;
};
: Person {
:
{ }
:
_stuid;
};
: Person {
:
{ }
:
string _title;
};
{
Student s;
Teacher t;
s.();
t.();
;
}
Person
public
void identity()
"void identity()"
protected
"张三"
int
18
class
Student
public
public
void study()
protected
int
class
Teacher
public
public
void teaching()
protected
int main()
identity
identity
return
0
1.2 继承定义
1.2.1 定义格式
- 基类(父类):被继承的类,如 Person。
- 派生类(子类):继承得到的新类,如 Student。
- 继承方式:public、protected、private,默认继承方式为 private(class 定义时)或 public(struct 定义时),建议显式写出。
1.2.2 继承基类成员访问方式的变化
| 类成员/继承方式 | public 继承 | protected 继承 | private 继承 |
|---|
| 基类的 public 成员 | 派生类的 public 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
| 基类的 protected 成员 | 派生类的 protected 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
| 基类的 private 成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
- 基类 private 成员在任何继承方式下,在派生类中均不可见(语法限制访问,实际仍被继承)。
- 若基类成员需在派生类中访问但禁止外部直接访问,应定义为 protected。
- 派生类成员访问权限 = Min(基类成员访问限定符,继承方式),优先级:public > protected > private。
- 实际开发中优先使用 public 继承,protected/private 继承因扩展性差极少使用。
class Person {
public:
void Print() { cout << _name << endl; }
protected:
string _name;
private:
int _age;
};
class Student : public Person {
protected:
int _stunum;
};
class Student : protected Person { ... };
class Student : private Person { ... };
1.3 继承类模板
当基类是类模板时,派生类需指定基类的类型,否则编译器无法识别标识符。
namespace gxy {
template<class T>
class stack : public std::vector<T> {
public:
void push(const T& x) {
vector<T>::push_back(x);
}
void pop() {
vector<T>::pop_back();
}
const T& top() {
return vector<T>::back();
}
bool empty() {
return vector<T>::empty();
}
};
}
int main() {
gxy::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty()) {
cout << st.top() << " ";
st.pop();
}
return 0;
}
namespace gxy {
template<class T, class CONTAINER = std::vector<T>>
class stack {
public:
void push(const T& x) {
CONTAINER::push_back(x);
}
void pop() {
CONTAINER::pop_back();
}
const T& top() {
return CONTAINER::back();
}
bool empty() const {
return CONTAINER::empty();
}
size_t size() const {
return CONTAINER::size();
}
private:
CONTAINER _c;
};
}
- 模板参数设计:
- T:栈中存储的元素类型。
- CONTAINER:底层容器类型,默认使用 std::vector,也可以指定为 std::deque、std::list 等。
- 使用示例:
#include <iostream>
#include <vector>
#include <deque>
int main() {
gxy::stack<int, std::vector<int>> st1;
st1.push(1);
st1.push(2);
st1.push(3);
while (!st1.empty()) {
std::cout << st1.top() << " ";
st1.pop();
}
std::cout << std::endl;
gxy::stack<int, std::deque<int>> st2;
st2.push(4);
st2.push(5);
st2.push(6);
while (!st2.empty()) {
std::cout << st2.top() << " ";
st2.pop();
}
return 0;
}
#define CONTAINER std::deque
#include <iostream>
#include <vector>
#include <list>
#include <deque>
using namespace std;
namespace gxy {
template<class T>
class stack : public CONTAINER<T> {
public:
void push(const T& x) {
CONTAINER<T>::push_back(x);
}
void pop() {
CONTAINER<T>::pop_back();
}
const T& top() {
return CONTAINER<T>::back();
}
bool empty() {
return CONTAINER<T>::empty();
}
};
}
int main() {
gxy::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
while (!st.empty()) {
cout << st.top() << " ";
st.pop();
}
cout << endl;
return 0;
}
2. 基类和派生类间的转换
- 派生类对象可以赋值给基类的对象/指针/引用(切片/切割,仅复制基类部分)。
- 基类对象不能赋值给派生类对象。
- 基类指针/引用可通过强制类型转换赋值给派生类指针/引用,但仅当基类指针实际指向派生类对象时才安全,可使用 dynamic_cast 进行安全识别。
class Person {
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person {
public:
int _No;
};
int main() {
Student sobj;
Person* pp = &sobj;
Person& rp = sobj;
Person pobj = sobj;
return 0;
}
#include <iostream>
#include <string>
using namespace std;
class Person {
virtual void func() {}
public:
string _name;
string _sex;
int _age;
};
class Student : public Person {
public:
int _No;
};
int main() {
Student sobj;
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
rp._name = "张三";
int i = 1;
double d = i;
const double& rd = i;
Student* ps1 = dynamic_cast<Student*>(pp);
cout << ps1 << endl;
pp = &pobj;
Student* ps2 = dynamic_cast<Student*>(pp);
cout << ps2 << endl;
return 0;
}
- 子类对象 → 父类对象/指针/引用 ✅ 允许,这叫 切片 / 切割(object slicing)
- 子类是'父类 + 自己扩展';
- 赋值给父类时,只拷贝父类那部分成员;
- 语法天然支持,不需要强转。
Person pobj = sobj; Person* pp = &sobj; Person& rp = sobj;
- 父类对象 → 子类对象 ❌ 不允许
- 父类成员少,没有子类的成员(如
_No);强行赋值会缺数据,语法直接报错。
// sobj = pobj; // 错误
- dynamic_cast<子类*>(父类指针):安全的向下转型;如果父指针真的指向子类对象 → 转换成功,返回有效地址;如果父指针指向父类对象 → 转换失败,返回 nullptr。
总结:子类 → 父类:天然允许(切片);父类 → 子类:不允许,不安全;dynamic_cast 用来安全判断是不是真的指向子类。
3. 继承中的作用域
3.1 隐藏规则
- 基类和派生类拥有独立的作用域。
- 若派生类与基类存在同名成员,派生类成员会隐藏基类成员的直接访问。
- 在派生类中可通过基类::成员显式访问被隐藏的基类成员。
- 成员函数隐藏仅需函数名相同,与参数列表无关。
- 实际开发中应避免在继承体系中定义同名成员。
#include <iostream>
#include <string>
using namespace std;
class Person {
protected:
string _name = "小李子";
int _num = 111;
};
class Student : public Person {
public:
void Print() {
cout << _num << endl;
cout << Person::_num << endl;
}
protected:
int _num = 999;
};
int main() {
Student s;
s.Print();
return 0;
}
- 只要同名,就会构成隐藏
- 子类有
_num,父类也有 _num;名字相同 = 隐藏,跟参数、类型都没关系
- 直接访问
_num
- 编译器查找顺序:先在自己类(Student)里找;找到了,就用自己的 → 999
- 想访问父类的同名成员
- 必须写:父类名::成员名
Person::_num 这样才能强制访问到父类的 111
总结:子类和父类成员同名 → 子类隐藏父类。直接访问是子类,加 父类:: 访问父类。
3.2 考察继承作用域相关选择题
1)题目 1
A 和 B 类中的两个 func 构成隐藏关系(B 继承 A,且 func 同名)。
以下程序编译运行结果为编译报错,因为 B::fun(int) 隐藏了 A::fun(),b.fun() 无法直接调用基类无参版本。
class A {
public:
void fun() { cout << "func()" << endl; }
};
class B : public A {
public:
void fun(int i) { cout << "func(int i)" << i << endl; }
};
int main() {
B b;
b.fun(10);
return 0;
}
#include <iostream>
using namespace std;
class A {
public:
void fun() { cout << "func()" << endl; }
};
class B : public A {
public:
void fun(int i) { cout << "func(int i)" << i << endl; }
};
int main() {
B b;
b.A::fun();
b.fun(1);
return 0;
}
- 只要函数名相同,就构成隐藏
- 父类:void fun();子类:void fun(int i);名字相同 = 隐藏;跟参数、返回值都没关系。
- 这不是重载!重载:同一个作用域,同名不同参
- 隐藏:不同作用域(父类/子类),只要同名就隐藏
- 所以:b.fun(); // 会报错!因为子类 fun(int) 把父类无参 fun() 隐藏了。
- 想调用父类被隐藏的函数
- 必须加:b.父类名::函数名 ();
b.A::fun();
总结:继承中,函数名相同就构成隐藏。子类会遮住父类,想调用父类必须加 父类名::。
4. 派生类的默认成员函数
4.1 4 个常见默认成员函数的生成规则
派生类的 6 个默认成员函数生成时,必须先处理基类部分:
- 构造函数:派生类构造函数必须调用基类构造函数初始化基类成员。若基类无默认构造函数,需在派生类初始化列表显式调用。
- 拷贝构造函数:派生类拷贝构造必须调用基类拷贝构造完成基类拷贝初始化。
- operator=:派生类赋值运算符必须调用基类 operator= 完成基类复制,且需显式指定基类作用域(因派生类 operator= 隐藏了基类版本)。
- 析构函数:派生类析构函数执行完成后,会自动调用基类析构函数清理基类成员,保证'先清理派生类,再清理基类'的顺序。
- 构造与析构顺序:
- 对象初始化:先调用基类构造,再调用派生类构造。
- 对象析构:先调用派生类析构,再调用基类析构。
- 基类析构函数未加 virtual 时,派生类与基类析构函数构成隐藏关系。
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
Person(const char* name = "xxx") : _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 {
public:
Student(const char* name, int num, const char* addrss) : Person(name)
, _num(num)
, _addrss(addrss) {
}
Student(const Student& s) : Person(s)
, _num(s._num)
, _addrss(s._addrss) {
}
Student& operator=(const Student& s) {
cout << "Student& operator=(const Student& s)" << endl;
if (this != &s) {
Person::operator=(s);
_num = s._num;
_addrss = s._addrss;
}
return *this;
}
~Student() {
cout << "~Student()" << endl;
}
protected:
int _num = 1;
string _addrss = "西安市高新区";
int* _ptr = new int[10];
};
int main() {
Student s1("张三", 1, "西安市");
Student s2(s1);
Student s3("李四", 2, "咸阳市");
s1 = s3;
return 0;
}
Person()
Person(const Person& p)
Person()
Person operator=(const Person& p)
Student& operator=(const Student& s)
~Student()
~Person()
~Student()
~Person()
~Student()
~Person()
- 子类构造:必须先调用父类构造;父类没有默认构造时,必须在初始化列表显式调用
- 子类拷贝构造:必须调用父类拷贝构造,写法:Person(s)
- 子类赋值重载:子类与父类的 operator= 构成隐藏,必须写:Person::operator=(s);
- 子类析构:不用手动调用父类析构,编译器会自动调父类析构,顺序:先析构子类→自动析构父类
解释 Person(s)
Person(s) 的意思就是:把子对象 s 里的'父类那一部分',拿去初始化父类。
- 先搞懂:子类对象里长什么样
class Person { ... };
class Student : public Person { ... };
一个 Student 对象里面,其实是两部分拼起来的:
Student 对象 s
├─ Person 部分(_name) ← 父类的成员
└─ 自己部分 (_num, _addrss) ← 子类的成员
- 拷贝构造要做什么?
拷贝构造:用一个现成的 s 对象,拷贝出一个一模一样的新对象。
那就要拷贝两部分:1. 父类那一半 2. 子类自己那一半
- 为什么要写 Person(s):父类的成员(_name)在子类里不能直接赋值初始化,必须调用父类的拷贝构造函数来初始化。
Student(const Student& s) : Person(s)
, _num(s._num)
, _addrss(s._addrss) {}
Person(s) 到底干了什么?把 s 对象里的父类部分,拿去初始化我这个新对象的父类部分。
- s 是子类对象
- 传给 Person 的拷贝构造时,会自动切片,只把里面 父类那一部分 拿出来
- 调用:Person(const Person& p)
- 把父类部分拷贝完成
- 不写会怎样?如果你写成:
Student(const Student& s) : _num(s._num), _addrss(s._addrss) {}
编译器会自动调用父类的默认构造,而不是拷贝构造!结果就是:子类成员拷贝对了,父类成员没有被拷贝,是随机值/默认值,这就是错的。
- 总结:子类拷贝构造必须写:: 父类名 (子类对象) 你这里就是:: Person(s) 意思:拷贝父类部分 → 必须调用父类拷贝构造,子类部分 → 自己正常初始化
class Student : public Person {
public:
protected:
int _num = 1;
string _addrss = "西安市高新区";
};
int main() {
Student s;
return 0;
}
继承的父类成员,会被看成一个整体对象,调用父类默认构造。
可以把 子类对象 理解成:
Student 对象 s
├─ 【一块整体:Person 父类部分】
├─ int _num
└─ string _addrss
重点:父类那一部分,在子类眼里,就是一个整体对象。
编译器默认生成的构造函数做什么?当你没写构造函数时,编译器自动生成的构造会干 3 件事:
- 父类部分:看成一个整体对象 → 调用父类的默认构造 Person()
- 自定义类型(如 string):→ 调用它自己的默认构造
- 内置类型(int、char)*:→ 不处理,值是随机的(除非你给了缺省值 =1)
结论:子类默认构造 → 自动调用父类默认构造!
Student s; 这句代码执行时:1. 先调用:Person() 2. 再初始化子类成员
什么时候会报错?如果父类 没有默认构造函数,比如:
class Person {
public:
Person(const char* name) { ... }
};
那写:Student s; 就会 编译报错!因为:子类默认构造要调用父类默认构造,但父类没有!
口诀:子类构造,必先调父类构造。没写就调默认构造,没有默认构造就报错。
4.2 实现一个不能被继承的类
- 方法 1(C++98):将基类构造函数私有化,派生类无法调用基类构造,从而无法实例化。
#include<iostream>
using namespace std;
class Base {
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
private:
Base() {}
};
class Derive : public Base {};
int main() {
return 0;
}
缺点:这种方式不仅禁止了继承,也禁止了在类外部创建 Base 的对象(相当于一个不可实例化的类)。
- 方法 2(C++11):使用 final 关键字修饰基类,直接禁止继承。
#include<iostream>
using namespace std;
class Base final {
public:
Base() {}
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};
class Derive : public Base {};
int main() {
Base b;
b.func5();
return 0;
}
- 语义明确,一眼就能看出是为了禁止继承。
- 基类的构造函数可以公开,允许创建 Base 类型的对象。
- 补充知识点:final 也可以修饰成员函数
除了修饰类,final 还可以修饰虚函数,禁止派生类重写该函数:
class Base {
public:
virtual void show() final { cout << "Base show" << endl; }
};
class Derive : public Base {
public:
};
- C++11 及以上:请直接使用 class Base final,这是最优雅的方案。
- C++98:只能通过将基类构造函数设为 private 来间接实现,但会导致基类无法实例化。
5. 继承与友元
友元关系不能继承,基类友元无法访问派生类的私有和保护成员。
友元关系不能被继承;友元关系不传递、不继承、不自动共享。
class Person {
friend void f();
};
class Student : public Person {};
f() 是 Person 的友元;但 f() 不是 Student 的友元;子类不会自动获得父类的友元关系。
这就叫:友元关系不能被继承。
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;
}
class Student : public Person {
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum;
};
- 访问 p._name 需要 Person 的友元;访问 s._stuNum 需要 Student 的友元
两个都要给,函数才能同时访问两个类的保护/私有成员。
最终可运行完整版:
#include<iostream>
#include<string>
using namespace std;
class Student;
class Person {
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name = "张三";
};
class Student : public Person {
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum = 10086;
};
void Display(const Person& p, const Student& s) {
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main() {
Person p;
Student s;
Display(p, s);
return 0;
}
总结:友元 不能继承;友元 不能传递;想访问谁的私有/保护,就必须成为谁的友元。
6. 继承与静态成员
基类定义的 static 静态成员,在整个继承体系中只有一个实例,所有派生类共享该成员。
- 普通成员变量:子类会继承一份,父子各有各的
- 静态成员变量:父子共用同一份,属于整个类家族
#include<iostream>
#include<string>
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;
cout << "&p._name = " << &p._name << endl;
cout << "&s._name = " << &s._name << endl;
cout << endl;
cout << "&p._count = " << &p._count << endl;
cout << "&s._count = " << &s._count << endl;
cout << endl;
cout << "Person::_count = " << Person::_count << endl;
cout << "Student::_count = " << Student::_count << endl;
cout << endl;
Person::_count++;
cout << "p._count = " << p._count << endl;
cout << "s._count = " << s._count << endl;
return 0;
}
- 普通成员(非 static)
string _name;
- 父类一个对象,子类一个对象;内存地址 不同;各自独立,互不影响。
- 静态成员(static)
static int _count;
- 静态成员属于类,不属于某个对象;继承后,子类和父类共享同一块内存;地址 完全相同;无论 p._count++ 还是 s._count++,改的是同一个变量。
- 访问方式都合法
Person::_count = 10;
Student::_count = 20;
p._count++;
s._count++;
- 这些写法全都可以,因为:静态成员可以通过 类名 访问;也可以通过 对象 访问;子类继承后,也能看到父类的静态成员
- 运行结果规律
&p._name != &s._name → 地址不同
&p._count == &s._count → 地址相同
- 只要改一次
_count,父类、子类、对象、类名访问 值全部一起变
- 最精炼总结
- 非静态成员:每个对象一份,继承后子类也有一份,独立存储。
- 静态成员:属于类,不属于对象,继承后父子共用一份。
- 友元不能继承,但静态成员可以继承且共享
7. 多继承及其菱形继承问题
7.1 继承模型
- 单继承:一个派生类只有一个直接基类。
- 多继承:一个派生类有两个或以上直接基类,内存模型为'先继承的基类在前,后继承的在后,派生类成员在最后'。
- 菱形继承:多继承的特殊情况,存在数据冗余和二义性问题(如 Assistant 继承 Student 和 Teacher,两者又继承 Person,导致 Person 成员存在两份)。
#include <iostream>
#include <string>
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() {
Assistant a;
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
cout << a.Student::_name << endl;
cout << a.Teacher::_name << endl;
return 0;
}
- 为什么报错?这是菱形继承(钻石继承):
Person
/ \
Student Teacher
\ /
Assistant
- Student 继承了一份 Person;Teacher 继承了一份 Person;Assistant 里就有两份 `_name`
- 直接写 `a._name`,编译器二义性,不知道选哪份
2. 两个问题:1)访问二义性:直接 `a._name` 报错;2)数据冗余:存了两份 Person 成员,浪费空间
3. 上述解法,显式指定类域:`a.Student::_name` `a.Teacher::_name`
- 只能解决二义性,解决不了数据冗余
4. 真正的解决方案 -> 虚继承:
```cpp
class Student : virtual public Person {};
class Teacher : virtual public Person {};
- 作用:让 Assistant 中只保留一份 Person;既解决二义性,又解决数据冗余。
- 总结:
- 普通菱形继承:两份基类 → 二义性 + 冗余
- 指定类域:只能解决二义性
- 虚继承:真正解决两个问题
7.2 虚继承
使用 virtual 关键字修饰继承,可解决菱形继承的数据冗余和二义性问题。
#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";
cout << a._name << endl;
return 0;
}
注意:虚继承底层实现复杂,会带来性能损失,实际开发中应避免设计菱形继承。
- 不加 virtual(普通菱形继承):Assistant 里有 两份 Person;
a._name 报错:访问不明确
- 加了 virtual(虚继承):最终子类里 只有一份 Person;
a._name 可以直接用,无歧义;同时解决:二义性 + 数据冗余
- 虚继承作用:让多个中间子类(Student、Teacher)共享同一份祖先类(Person)
#include<iostream>
#include<string>
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 = 0) : Person(name)
,_num(num) {}
protected:
int _num;
};
class Teacher : virtual public Person {
public:
Teacher(const char* name, int id = 1) : Person(name)
, _id(id) {}
protected:
int _id;
};
class Assistant : public Student, public Teacher {
public:
Assistant(const char* name1, const char* name2, const char* name3)
: Student(name1)
, Teacher(name2)
, Person(name3)
{
}
protected:
string _majorCourse;
};
int main() {
Assistant a("张三", "李四", "王五");
a._name = "peter";
cout << a._name << endl;
return 0;
}
- 共同基类 Person
class Person {
public:
Person(const char* name) :_name(name) {}
string _name;
};
这是最顶层的基类,只有一个成员:_name,只有有参构造,没有无参构造
- 虚继承:Student / Teacher
class Student : virtual public Person {
public:
Student(const char* name, int num = 0) : Person(name)
,_num(num) {}
protected:
int _num;
};
class Teacher : virtual public Person {
public:
Teacher(const char* name, int id = 1) : Person(name)
,_id(id) {}
protected:
int _id;
};
关键点:virtual public Person = 虚继承
意义:告诉编译器,如果以后有多继承,大家共用这一份 Person
重点规则:虚继承时,中间子类(Student/Teacher)的构造函数,不会初始化 Person!只会由 最终最底层子类 来初始化。
- 最终子类:Assistant(菱形结构)
class Assistant : public Student, public Teacher {
public:
Assistant(const char* name1, const char* name2, const char* name3)
: Student(name1)
, Teacher(name2)
, Person(name3)
{}
protected:
string _majorCourse;
};
虚继承的强制规则:
- 虚继承的顶层基类(Person),必须由最终子类直接初始化
- Student(name1)、Teacher(name2) 里的 Person(name) 都不会执行
- 只有你写的 Person(name3) 会真正构造基类
这就是为什么你要传三个名字,但只有最后一个名字生效。
- main 函数里发生了什么
int main() {
Assistant a("张三", "李四", "王五");
a._name = "peter";
cout << a._name << endl;
return 0;
}
没有虚继承会怎样?Assistant 里面会有 两份 Person,一份来自 Student,一份来自 Teacher,访问 a._name → 编译报错:二义性
有了虚继承:内存中 只有一份 Person,a._name 可以直接访问,不报错,解决:二义性、数据冗余
- name1:传给 Student → 没用,不会构造 Person
- name2:传给 Teacher → 没用,不会构造 Person
- name3:传给 Person → 只有它真正生效
逐行拆开看
Assistant a("张三", "李四", "王五");
对应构造函数:
Assistant(const char* name1, const char* name2, const char* name3)
: Student(name1)
, Teacher(name2)
, Person(name3)
- name1 = "张三":传给 Student,但因为是 虚继承,Student 不能去构造 Person,所以这个名字白传了,没用
- name2 = "李四":传给 Teacher,同样因为虚继承,Teacher 也不能构造 Person,这个名字也没用
- name3 = "王五":直接传给 Person 构造函数
虚继承规定:最终子类必须亲自初始化最顶层基类,所以 只有 name3 真正初始化
_name
最终结果:a._name 最终 = 王五;张三、李四都没起作用
虚继承菱形继承中:中间类(Student、Teacher)不能构造公共基类,只有最底下的孙子类(Assistant)才能构造 Person
所以:name1、name2 是摆设;name3 才是真正给 Person 的名字
- 普通菱形继承的问题
- 二义性:
a._name 不知道是 Student 的还是 Teacher 的;数据冗余:Person 成员存了两份
- 虚继承 virtual public 做了什么:让共同基类 Person 只存在一份;解决二义性 + 数据冗余
- 虚继承构造函数规则(重点):中间类 (Student、Teacher) 的 Person(name) 不执行;必须由 最终派生类 (Assistant) 直接初始化 Person
- 菱形继承:Person 被继承两次 → 两份数据 → 二义性 + 冗余
- 虚继承构造规则(最重要):中间子类(Student/Teacher)不构造顶层基类,必须由最终子类直接构造顶层基类
总结:虚继承就是为了解决菱形继承的二义性和数据冗余,且虚继承时,最顶层基类必须由最终子类直接构造。
7.3 多继承 + 指针切片 + 地址偏移
#include<iostream>
using namespace std;
class Base1 {
public:
int _b1 = 1;
};
class Base2 {
public:
int _b2 = 2;
};
class Derive : public Base2, public Base1 {
public:
int _d = 3;
int _e = 4;
};
int main() {
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
cout << "Derive* p3: " << p3 << endl;
cout << "Base2* p2: " << p2 << endl;
cout << "Base1* p1: " << p1 << endl;
cout << "sizeof(Base1) = " << sizeof(Base1) << endl;
cout << "sizeof(Base2) = " << sizeof(Base2) << endl;
cout << "sizeof(Derive) = " << sizeof(Derive) << endl;
return 0;
}
- 内存布局(重点) 你写的是:
class Derive : public Base2, public Base1
继承顺序是:先 Base2,再 Base1,最后自己成员;所以 Derive 内存布局是:
低地址 ---------------------------> 高地址
+-------------------------+
| Base2::_b2 = 2 | // 先放第一个父类 Base2
+-------------------------+
| Base1::_b1 = 1 | // 再放第二个父类 Base1
+-------------------------+
| Derive::_d = 3 | // 自己的成员
+-------------------------+
| Derive::_e = 4 |
+-------------------------+
一共:4 + 4 + 4 + 4 = 16 字节
- 三个指针的地址区别
Derive* p3 = &d
Base2* p2 = &d
Base1* p1 = &d
Derive* p3 ↓
+-------------------------+
| Base2::_b2 = 2 | ← p2 (Base2*) 也指向这里
+-------------------------+
| Base1::_b1 = 1 | ← p1 (Base1*) 指向这里
+-------------------------+
| Derive::_d = 3 |
+-------------------------+
| Derive::_e = 4 |
+-------------------------+
所以你会看到:p3 == p2(地址一样);p1 比 t3 大 4(偏移了一个 int)
这就是多继承下,父类指针会自动偏移到对应子对象位置。
- 多继承时,子类对象会按继承顺序,把所有父类对象依次放在前面
- 不同父类指针指向同一个子类对象时,地址可能不一样(会自动偏移)
- 但它们都指向同一个对象的不同部分
8. 继承和组合
8.1 继承和组合的区别
| 特性 | 继承(public) | 组合 |
|---|
| 关系 | is-a(派生类是一个基类) | has-a(类包含其他类对象) |
| 复用方式 | 白箱复用(基类内部细节对派生类可见) | 黑箱复用(被组合对象内部细节不可见) |
| 耦合度 | 高(基类改变影响派生类) | 低(对象间依赖弱) |
| 封装性 | 一定程度破坏封装 | 保持良好封装 |
| 优先选择 | 适合 is-a 关系或实现多态 | 优先使用,代码维护性好 |
#include<iostream>
#include<string>
#include<vector>
using namespace std;
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; }
};
class Benz : public Car {
public:
void Drive() { cout << "好坐 - 舒适" << endl; }
};
template<class T>
class stack : public vector<T> {
};
template<class T>
class stack {
public:
private:
vector<T> _v;
};
- 继承:is-a(是一个),满足'是一种'才用继承
- 例:BMW is a Car ✅ Student is a Person ✅ Cat is a Animal ✅
- 组合:has-a(有一个),满足'有一个'才用组合
- 例:Car has a Tire ✅ Person has a heart ✅ stack has a vector
- 继承会把父类所有接口都继承下来
- stack 本来只应该:push、pop、top;如果继承 vector,就会拥有 insert、erase、[] 等不该有的接口;破坏封装,不安全
- 组合低耦合、更安全
- 只对外暴露栈的接口;底层用 vector 实现,但对外不可见;高内聚、低耦合
总结:is-a 关系 → 用继承;has-a 关系 → 用组合;能用组合就尽量不用继承 (组合耦合更低、更安全)
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online