继承作为 C++ 面向对象三大特性之一,是实现代码复用与扩展的核心机制,也是面试与工程开发中的高频考点。本文将从继承的基本概念、语法规则、成员访问控制、默认成员函数、多继承与菱形继承问题,到继承与组合的选型,带你系统性吃透 C++ 继承,彻底搞懂底层原理与实战坑点。
1. 继承的概念及定义
1.1 继承的概念
继承(inheritance)是面向对象程序设计中代码复用的重要手段,允许在保持原有类特性的基础上进行扩展,产生新的类(派生类/子类)。它体现了面向对象的层次结构,实现了类设计层次的复用。
示例场景:
• 未使用继承时,Student和Teacher类中存在大量冗余代码(如姓名、地址、电话、年龄等成员变量,以及身份认证identity()成员函数)。
• 使用继承后,可将公共成员提取到Person基类中,Student和Teacher作为派生类继承Person,避免重复定义。
cpp
// 未使用继承的冗余代码
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 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 继承定义
1.2.1 定义格式
cpp
class 派生类 : 继承方式 基类 {
// 新增成员
};
• 基类(父类):被继承的类,如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继承因扩展性差极少使用。
cpp
// 三种继承方式示例
class Person {
public:
void Print() { cout << _name << endl; }
protected:
string _name;
private:
int _age;
};
// public继承
class Student : public Person {
protected:
int _stunum;
};
// protected继承
// class Student : protected Person { ... };
// private继承
// class Student : private Person { ... };
1.3 继承类模板
当基类是类模板时,派生类需指定基类的类型,否则编译器无法识别标识符。
cpp
namespace gxy {
// stack通过public继承vector实现,既符合is-a,也符合has-a
template<class T>
class stack : public std::vector<T> {
public:
void push(const T& x) {
// 需指定类域,否则编译报错:error C3861: "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();
}
};
}
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;
}
可适配不同底层容器的栈(stack)
cpp
namespace gxy
{
// 模板参数 CONTAINER 表示栈所使用的底层容器
// 它必须支持 push_back、pop_back、back、empty 等接口
template<class T, class CONTAINER = std::vector<T>>
class stack
{
public:
// 入栈:在容器尾部添加元素
void push(const T& x)
{
// 显式调用底层容器的 push_back
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<T>,也可以指定为 std::deque<T>、std::list<T> 等。
- 使用示例:
cpp
#include <iostream>
#include <vector>
#include <deque>
int main()
{
// 使用 vector 作为底层容器
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;
// 使用 deque 作为底层容器
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;
}
用宏切换底层容器
cpp
// 用宏切换底层容器:vector / list / deque
//#define CONTAINER std::vector
//#define CONTAINER std::list
#define CONTAINER std::deque
// 在预处理阶段,编译器会把所有 CONTAINER<T> 直接替换成 std::deque<T>
#include <iostream>
#include <vector>
#include <list>
#include <deque>
using namespace std;
// 自己命名空间
namespace gxy
{
// stack 继承自底层容器(教学演示用)
template<class T>
class stack : public CONTAINER<T>
{
public:
// 入栈:调用容器的尾插
void push(const T& x)
{
// 必须加 容器名<T>:: 因为是依赖基类模板
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. 基类和派生类间的转换
• public继承下的赋值兼容规则:
-
派生类对象可以赋值给基类的对象/指针/引用(切片/切割,仅复制基类部分)。
-
基类对象不能赋值给派生类对象。
-
基类指针/引用可通过强制类型转换赋值给派生类指针/引用,但仅当基类指针实际指向派生类对象时才安全,可使用dynamic_cast进行安全识别。
cpp
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;
}
cpp
#include <iostream>
#include <string>
using namespace std;
// 父类
class Person
{
// 虚函数,后面多态会用
virtual void func()
{}
public:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
// 子类:公有继承 Person
class Student : public Person
{
public:
int _No; // 学号
};
int main()
{
Student sobj; // 子类对象
// ==========================
// 1. 赋值兼容规则(最核心)
// 子类对象 → 可以赋值给 父类对象 / 父类指针 / 父类引用
// 这个动作叫:切片 / 切割(slicing)
// ==========================
// 子类对象 赋值给 父类对象
// 只会拷贝父类那部分成员,子类独有成员会被切掉
Person pobj = sobj;
// 父类指针 指向 子类对象
Person* pp = &sobj;
// 父类引用 引用 子类对象
Person& rp = sobj;
rp._name = "张三"; // 通过父类引用修改子类对象里的父类部分
// ==========================
// 对比理解:普通类型隐式转换
// ==========================
int i = 1;
double d = i; // int 隐式转 double
const double& rd = i; // 引用也支持隐式转换(会产生临时量)
// ==========================
// 2. 父类对象 → 不能赋值给子类对象
// 编译报错!
// 因为父类少成员(没有_No),塞不进子类
// ==========================
// sobj = (Student)pobj; // 错误!
// ==========================
// 3. dynamic_cast 安全向下转型
// ==========================
// pp 原本指向子类对象 sobj
// 可以安全转成 Student*
Student* ps1 = dynamic_cast<Student*>(pp);
cout << ps1 << endl; // 输出有效地址,非空
// pp 现在指向父类对象 pobj
// 强转成 Student* 会失败,返回 nullptr
pp = &pobj;
Student* ps2 = dynamic_cast<Student*>(pp);
cout << ps2 << endl; // 输出 0(空指针)
return 0;
}
最核心的 3 个知识点
- 子类对象 → 父类对象/指针/引用 ✅ 允许,这叫 切片 / 切割(object slicing)
子类是"父类+自己扩展"; 赋值给父类时,只拷贝父类那部分成员; 语法天然支持,不需要强转。
Person pobj = sobj; Person* pp = &sobj; Person& rp = sobj;
- 父类对象 → 子类对象 ❌ 不允许
父类成员少,没有子类的成员(如 _No);强行赋值会缺数据,语法直接报错。// sobj = pobj; // 错误
- dynamic_cast<子类*>(父类指针):安全的向下转型;如果父指针真的指向子类对象 → 转换成功,返回有效地址;如果父指针指向父类对象 → 转换失败,返回 nullptr。
总结:子类 → 父类:天然允许(切片) 父类 → 子类:不允许,不安全
dynamic_cast 用来安全判断是不是真的指向子类
3. 继承中的作用域
3.1 隐藏规则
-
基类和派生类拥有独立的作用域。
-
若派生类与基类存在同名成员,派生类成员会隐藏基类成员的直接访问。
-
在派生类中可通过基类::成员显式访问被隐藏的基类成员。
-
成员函数隐藏仅需函数名相同,与参数列表无关。
-
实际开发中应避免在继承体系中定义同名成员。
cpp
#include <iostream>
#include <string>
using namespace std;
// 父类
class Person
{
protected:
string _name = "小李子"; // 姓名
int _num = 111; // 身份证号(父类的 _num)
};
// 子类公有继承 Person
class Student : public Person
{
public:
void Print()
{
// 这里直接访问 _num:访问的是【子类自己】的 _num
// 因为子类和父类同名成员,子类会【隐藏】父类
cout << _num << endl; // 输出:999
// 想访问父类的 _num:必须加 【父类名::】
cout << Person::_num << endl; // 输出:111
}
protected:
int _num = 999; // 学号(子类自己的 _num,和父类同名)
};
int main()
{
Student s;
s.Print();
return 0;
}
运行结果:
cpp
999
111
核心知识点:同名成员的隐藏规则
- 只要同名,就会构成隐藏
子类有 _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()无法直接调用基类无参版本。
cpp
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); // 正确,调用B::fun(int)
// b.fun(); // 编译报错,A::fun()被隐藏
return 0;
}
2)题目2
cpp
#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;
// 1. 加类名限定,调用父类的 fun()
b.A::fun();
// 2. 调用子类的 fun(int)
b.fun(1);
return 0;
}
输出结果:
cpp
func()
func(int i)1
核心考点:继承中的 隐藏 / 重定义
- 只要函数名相同,就构成隐藏
父类:void fun();子类:void fun(int i);名字相同 = 隐藏;跟参数、返回值都没关系。
- 这不是重载! 重载:同一个作用域,同名不同参
隐藏:不同作用域(父类/子类),只要同名就隐藏
所以:b.fun(); // 会报错! 因为子类 fun(int) 把父类无参 fun() 隐藏了。
- 想调用父类被隐藏的函数
必须加:b.父类名::函数名(); b.A::fun();
总结:继承中,函数名相同就构成隐藏。子类会遮住父类,想调用父类必须加 父类名::。
4. 派生类的默认成员函数
4.1 4个常见默认成员函数的生成规则
派生类的6个默认成员函数生成时,必须先处理基类部分:
-
构造函数:派生类构造函数必须调用基类构造函数初始化基类成员。若基类无默认构造函数,需在派生类初始化列表显式调用。
-
拷贝构造函数:派生类拷贝构造必须调用基类拷贝构造完成基类拷贝初始化。
-
operator=:派生类赋值运算符必须调用基类operator=完成基类复制,且需显式指定基类作用域(因派生类operator=隐藏了基类版本)。
-
析构函数:派生类析构函数执行完成后,会自动调用基类析构函数清理基类成员,保证"先清理派生类,再清理基类"的顺序。
-
构造与析构顺序:
◦ 对象初始化:先调用基类构造,再调用派生类构造。
◦ 对象析构:先调用派生类析构,再调用基类析构。
- 基类析构函数未加virtual时,派生类与基类析构函数构成隐藏关系。
代码1
cpp
#include <iostream>
#include <string>
using namespace std;
// 父类 Person
class Person
{
public:
// 1. 构造函数
Person(const char* name = "xxx")
: _name(name)
{
cout << "Person()" << endl;
}
// 2. 拷贝构造函数
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
// 3. 赋值重载 operator=
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
// 4. 析构函数
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
// 子类 Student 公有继承 Person
class Student : public Person
{
public:
// 子类构造函数
// 必须先在初始化列表调用 父类构造函数 Person(name)
Student(const char* name, int num, const char* addrss)
: Person(name) // 初始化父类部分
, _num(num) // 初始化子类成员
, _addrss(addrss)
{
// 如果父类没有默认构造,这里必须显式调用父类构造,否则编译报错
}
// 子类拷贝构造
// 必须调用 父类拷贝构造 Person(s)
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)
{
// 关键:
// 子类和父类的 operator= 构成隐藏关系
// 必须显式指定 父类::operator= 才能调用父类赋值
Person::operator=(s);
// 拷贝子类自己的成员
_num = s._num;
_addrss = s._addrss;
}
return *this;
}
// 子类析构函数
~Student()
{
cout << "~Student()" << endl;
// 重点规则:
// 1. 子类析构函数 会 自动调用 父类析构 ~Person()
// 2. 不需要我们手动写 Person::~Person();
// 3. 析构顺序:先析构子类,再自动析构父类(保证安全)
// 如果有动态申请的内存,在这里释放
// delete[] _ptr;
}
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;
}
输出结果:
cpp
Person() // s1 构造
Person(const Person& p) // s2 拷贝构造
Person() // s3 构造
Person operator=(const Person& p) // 赋值时先赋值父类
Student& operator=(const Student& s) // 再赋值子类
~Student() // s3 析构
~Person()
~Student() // s2 析构
~Person()
~Student() // s1 析构
~Person()
-
子类构造:必须先调用父类构造;父类没有默认构造时,必须在初始化列表显式调用
-
子类拷贝构造:必须调用父类拷贝构造,写法:Person(s)
-
子类赋值重载:子类与父类的 operator= 构成隐藏,必须写:Person::operator=(s);
-
子类析构:不用手动调用父类析构,编译器会自动调父类析构,顺序:先析构子类→自动析构父类
解释 Person(s)
Person(s) 的意思就是:把子对象 s 里的"父类那一部分",拿去初始化父类。
- 先搞懂:子类对象里长什么样
class Person { ... }; // 父类
class Student : public Person { ... }; // 子类
一个 Student 对象里面,其实是两部分拼起来的:
cpp
Student 对象 s
├─ Person 部分(_name) ← 父类的成员
└─ 自己部分 (_num, _addrss) ← 子类的成员
- 拷贝构造要做什么?
拷贝构造:用一个现成的 s 对象,拷贝出一个一模一样的新对象。
那就要拷贝两部分:1. 父类那一半 2. 子类自己那一半
- 为什么要写 Person(s):父类的成员(_name)在子类里不能直接赋值初始化,必须调用父类的拷贝构造函数来初始化。
cpp
Student(const Student& s)
: Person(s) // 👈 就是这一句
, _num(s._num)
, _addrss(s._addrss)
{}
Person(s) 到底干了什么?把 s 对象里的父类部分,拿去初始化我这个新对象的父类部分。
1) s 是子类对象
2) 传给 Person 的拷贝构造时,会自动切片,只把里面 父类那一部分 拿出来
3) 调用:Person(const Person& p)
4) 把父类部分拷贝完成
- 不写会怎样?如果你写成:
cpp
Student(const Student& s)
: _num(s._num)
, _addrss(s._addrss)
{}
编译器会自动调用父类的默认构造,而不是拷贝构造!结果就是:子类成员拷贝对了, 父类成员没有被拷贝,是随机值/默认值,这就是错的。
- 总结:子类拷贝构造必须写:: 父类名(子类对象) 你这里就是: : Person(s)
意思:拷贝父类部分 → 必须调用父类拷贝构造,子类部分 → 自己正常初始化
代码2
cpp
// 子类 Student 继承 Person
class Student : public Person
{
public:
// 默认生成的构造函数行为:
// 1、内置类型->不确定
// 2、自定义类型->调用默认构造
// 3、继承的父类成员看做一个整体对象,会调用父类的默认构造
protected:
int _num = 1;
string _addrss = "西安市高新区";
};
int main()
{
Student s;
return 0;
}
继承的父类成员,会被看成一个整体对象,调用父类默认构造。
可以把 子类对象 理解成:
cpp
Student 对象 s
├─ 【一块整体:Person 父类部分】
├─ int _num
└─ string _addrss
重点:父类那一部分,在子类眼里,就是一个整体对象。
编译器默认生成的构造函数做什么?当你没写构造函数时,编译器自动生成的构造会干 3 件事:
-
父类部分:看成一个整体对象 → 调用父类的默认构造 Person()
-
自定义类型(如 string):→ 调用它自己的默认构造
-
内置类型(int、char)*:→ 不处理,值是随机的(除非你给了缺省值 =1)
结论:子类默认构造 → 自动调用父类默认构造!
Student s; 这句代码执行时:1. 先调用:Person() 2. 再初始化子类成员
什么时候会报错?如果父类 没有默认构造函数,比如:
cpp
class Person
{
public:
// 只有带参构造,没有无参构造
Person(const char* name)
{ ... }
};
那写:Student s; 就会 编译报错! 因为:子类默认构造要调用父类默认构造,但父类没有!
口诀:子类构造,必先调父类构造。没写就调默认构造,没有默认构造就报错。
4.2 实现一个不能被继承的类
• 方法1(C++98):将基类构造函数私有化,派生类无法调用基类构造,从而无法实例化。
cpp
#include<iostream>
using namespace std;
class Base
{
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
private:
// 核心:构造函数私有化
Base() {}
};
// 【编译报错】无法从 Base 继承
// class Derive : public Base {};
int main()
{
// 【编译报错】外部也无法创建 Base 对象
// Base b;
return 0;
}
缺点:这种方式不仅禁止了继承,也禁止了在类外部创建 Base 的对象(相当于一个不可实例化的类)。
• 方法2(C++11):使用final关键字修饰基类,直接禁止继承。
cpp
#include<iostream>
using namespace std;
// 核心:使用 final 关键字修饰类
class Base final
{
public:
// 构造函数公开,允许创建对象
Base() {}
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};
// 【编译报错】error: cannot derive from 'final' type 'Base'
// class Derive : public Base {};
int main()
{
// 正常:Base 自身可以创建对象
Base b;
b.func5();
return 0;
}
优点:
-
语义明确,一眼就能看出是为了禁止继承。
-
基类的构造函数可以公开,允许创建 Base 类型的对象。
-
补充知识点:final 也可以修饰成员函数
除了修饰类,final 还可以修饰虚函数,禁止派生类重写该函数:
cpp
class Base
{
public:
// 禁止派生类重写 show 函数
virtual void show() final {
cout << "Base show" << endl;
}
};
class Derive : public Base
{
public:
// 【编译报错】无法重写 final 函数
// void show() override {}
};
总结:
• C++11 及以上:请直接使用 class Base final,这是最优雅的方案。
• C++98:只能通过将基类构造函数设为 private 来间接实现,但会导致基类无法实例化。
5. 继承与友元
友元关系不能继承,基类友元无法访问派生类的私有和保护成员。
友元关系不能被继承;友元关系不传递、不继承、不自动共享。
cpp
class Person
{
friend void f();
};
class Student : public Person
{};
f() 是 Person 的友元;但 f() 不是 Student 的友元;子类不会自动获得父类的友元关系。
这就叫:友元关系不能被继承。
代码示例:
cpp
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; // 正确,可访问基类protected成员
// cout << s._stuNum << endl; // 编译报错,无法访问派生类protected成员
}
// 解决方案:将Display声明为Student的友元
// class Student : public Person {
// friend void Display(const Person& p, const Student& s);
// protected:
// int _stuNum;
// };
• 访问 p._name 需要 Person 的友元; 访问 s._stuNum 需要 Student 的友元
两个都要给,函数才能同时访问两个类的保护/私有成员。
最终可运行完整版:
cpp
#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静态成员,在整个继承体系中只有一个实例,所有派生类共享该成员。
• 普通成员变量:子类会继承一份,父子各有各的
• 静态成员变量:父子共用同一份,属于整个类家族
代码示例:
cpp
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
string _name; // 非静态成员变量:属于每个对象
static int _count; // 静态成员变量:属于整个类,所有对象共享
};
// 静态成员变量必须在类外初始化
int Person::_count = 0;
// 子类公有继承 Person
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++;
这些写法全都可以,因为: 静态成员可以通过 类名 访问; 也可以通过 对象 访问; 子类继承后,也能看到父类的静态成员
- 运行结果规律
1) &p._name != &s._name → 地址不同
2) &p._count == &s._count → 地址相同
3) 只要改一次 _count,父类、子类、对象、类名访问 值全部一起变
- 最精炼总结
1) 非静态成员:每个对象一份,继承后子类也有一份,独立存储。
2) 静态成员:属于类,不属于对象,继承后父子共用一份。
3) 友元不能继承,但静态成员可以继承且共享
7. 多继承及其菱形继承问题
7.1 继承模型
• 单继承:一个派生类只有一个直接基类。
• 多继承:一个派生类有两个或以上直接基类,内存模型为"先继承的基类在前,后继承的在后,派生类成员在最后"。
• 菱形继承:多继承的特殊情况,存在数据冗余和二义性问题(如Assistant继承Student和Teacher,两者又继承Person,导致Person成员存在两份)。
cpp
#include <iostream>
#include <string>
using namespace std;
// 公共基类
class Person {
public:
string _name; // 姓名
};
// Student 继承 Person
class Student : public Person {
protected:
int _num; // 学号
};
// Teacher 继承 Person
class Teacher : public Person {
protected:
int _id; // 职工号
};
// 菱形继承:Assistant 同时继承 Student 和 Teacher
class Assistant : public Student, public Teacher {
protected:
string _majorCourse; // 主修课程
};
int main() {
Assistant a;
// 错误写法:编译报错
// error: 对"_name"的访问不明确
// 因为 a 里面有两份 _name:一份来自 Student,一份来自 Teacher
// 编译器不知道你要访问哪一个
// a._name = "peter";
// 正确写法:显式指定类域,指明访问哪个父类的 _name
a.Student::_name = "xxx"; // 访问 Student 继承下来的 _name
a.Teacher::_name = "yyy"; // 访问 Teacher 继承下来的 _name
// 验证:两个 _name 是独立的,互不干扰
cout << a.Student::_name << endl; // 输出 xxx
cout << a.Teacher::_name << endl; // 输出 yyy
return 0;
}
核心原理
- 为什么报错?这是菱形继承(钻石继承):
cpp
Person
/ \
Student Teacher
\ /
Assistant
Student 继承了一份 Person;Teacher 继承了一份 Person;Assistant 里就有两份 _name
直接写 a._name,编译器二义性,不知道选哪份
-
两个问题:1)访问二义性:直接 a._name 报错 ;2)数据冗余:存了两份 Person 成员,浪费空间
-
上述解法,显式指定类域:a.Student::_name a.Teacher::_name
只能解决二义性, 解决不了数据冗余
- 真正的解决方案 -> 虚继承:
cpp
class Student : virtual public Person {};
class Teacher : virtual public Person {};
作用:让 Assistant 中只保留一份 Person; 既解决二义性,又解决数据冗余。
- 总结:
普通菱形继承:两份基类 → 二义性 + 冗余;指定类域:只能解决二义性;虚继承:真正解决两个问题
7.2 虚继承
使用virtual关键字修饰继承,可解决菱形继承的数据冗余和二义性问题。
代码1
cpp
#include <iostream>
#include <string>
using namespace std;
// 公共基类
class Person {
public:
string _name;
};
// 虚继承 Person
// 告诉编译器:后续如果有多继承,共享这一份 Person
class Student : virtual public Person {
protected:
int _num;
};
// 虚继承 Person
class Teacher : virtual public Person {
protected:
int _id;
};
// 多继承:Assistant 同时继承 Student + Teacher
class Assistant : public Student, public Teacher {
protected:
string _majorCourse;
};
int main() {
Assistant a;
// 虚继承后:
// Person 部分 只存在一份
// 不再有二义性,可以直接访问
a._name = "peter";
cout << a._name << endl;
return 0;
}
注意:虚继承底层实现复杂,会带来性能损失,实际开发中应避免设计菱形继承。
核心知识点
-
不加 virtual(普通菱形继承):Assistant 里有 两份 Person; a._name 报错:访问不明确
-
加了 virtual(虚继承):最终子类里 只有一份 Person;a._name 可以直接用,无歧义;同时解决:二义性 + 数据冗余
-
虚继承作用: 让多个中间子类(Student、Teacher)共享同一份祖先类(Person)
代码2
cpp
#include<iostream>
#include<string>
using namespace std;
// 共同基类
class Person
{
public:
// 有参构造函数
Person(const char* name)
:_name(name)
{}
string _name; // 姓名
/*int _tel;
int _age;
string _gender;
string _address;*/
// ... 如果成员很多,普通菱形继承会造成大量数据冗余
};
// 虚继承 Person
class Student : virtual public Person
{
public:
Student(const char* name, int num = 0)
:Person(name) // 虚继承下,这里的 Person(name) 不会执行
,_num(num)
{}
protected:
int _num; //学号
};
// 虚继承 Person
class Teacher : virtual public Person
{
public:
Teacher(const char* name, int id = 1)
:Person(name) // 虚继承下,这里的 Person(name) 不会执行
, _id(id)
{}
protected:
int _id; // 职工编号
};
// 菱形继承:Assistant 同时继承 Student + Teacher
// 实际开发中:不要去玩菱形继承
class Assistant : public Student, public Teacher
{
public:
// 虚继承后,必须由【最终派生类】直接初始化 共同基类 Person
Assistant(const char* name1, const char* name2, const char* name3)
:Student(name1) // 这里不会初始化 Person
,Teacher(name2) // 这里不会初始化 Person
,Person(name3) // 只有这里会真正初始化 Person(虚继承规定)
{
// a._name 最终 = name3("王五")
}
protected:
string _majorCourse; // 主修课程
};
int main()
{
Assistant a("张三", "李四", "王五");
// 没有虚继承前:
// 编译报错:error C2385: 对"_name"的访问不明确
// 因为 Student 和 Teacher 各有一份 _name
// 有了虚继承后:
// 只有一份 _name,由 Assistant 直接初始化 Person
a._name = "peter"; // 不再二义性
cout << a._name << endl; // 正常访问
// 虚继承解决了两个问题:
// 1. 访问二义性
// 2. 数据冗余(只存一份Person成员)
return 0;
}
代码解释
- 共同基类 Person
cpp
class Person
{
public:
// 有参构造
Person(const char* name)
:_name(name)
{}
string _name; // 姓名
};
这是最顶层的基类, 只有一个成员:_name, 只有有参构造,没有无参构造
- 虚继承:Student / Teacher
cpp
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(菱形结构)
cpp
class Assistant : public Student, public Teacher
{
public:
Assistant(const char* name1, const char* name2, const char* name3)
:Student(name1) // 不初始化 Person
,Teacher(name2) // 不初始化 Person
,Person(name3) // 只有这里真正初始化 Person!
{}
protected:
string _majorCourse;
};
虚继承的强制规则:
1) 虚继承的顶层基类(Person),必须由最终子类直接初始化
2) Student(name1)、Teacher(name2) 里的 Person(name) 都不会执行
3) 只有你写的 Person(name3) 会真正构造基类
这就是为什么你要传三个名字,但只有最后一个名字生效。
- main 函数里发生了什么
cpp
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("张三", "李四", "王五");
对应构造函数:
cpp
Assistant(const char* name1, const char* name2, const char* name3)
:Student(name1) // name1 = "张三"
,Teacher(name2) // name2 = "李四"
,Person(name3) // name3 = "王五"
1) name1 = "张三": 传给 Student,但因为是 虚继承,Student 不能去构造 Person,所以这个名字白传了,没用
2) name2 = "李四": 传给 Teacher,同样因为虚继承,Teacher 也不能构造 Person,这个名字也没用
3) 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 多继承 + 指针切片 + 地址偏移
cpp
#include<iostream>
using namespace std;
class Base1 {
public:
int _b1 = 1; // 4字节
};
class Base2 {
public:
int _b2 = 2; // 4字节
};
// 多继承:先继承 Base2,再继承 Base1
class Derive : public Base2, public Base1 {
public:
int _d = 3; // 4字节
int _e = 4; // 4字节
};
int main()
{
Derive d;
// 这三个指针 指向同一个对象d,但类型不同,地址可能不同!
Base1* p1 = &d; // 指向对象中 Base1 部分
Base2* p2 = &d; // 指向对象中 Base2 部分
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 内存布局是:
cpp
低地址 ---------------------------> 高地址
+-------------------------+
| Base2::_b2 = 2 | // 先放第一个父类 Base2
+-------------------------+
| Base1::_b1 = 1 | // 再放第二个父类 Base1
+-------------------------+
| Derive::_d = 3 | // 自己的成员
+-------------------------+
| Derive::_e = 4 |
+-------------------------+
一共:4 + 4 + 4 + 4 = 16 字节
- 三个指针的地址区别
cpp
Derive* p3 = &d; → 指向整个对象**开头**
Base2* p2 = &d; → 也指向**开头**(因为Base2在最前)
Base1* p1 = &d; → 指向中间:p3 + 4 字节
p3 (Derive*)
↓
+-------------------------+
| Base2::_b2 = 2 | ← p2 (Base2*) 也指向这里
+-------------------------+
| Base1::_b1 = 1 | ← p1 (Base1*) 指向这里
+-------------------------+
| Derive::_d = 3 |
+-------------------------+
| Derive::_e = 4 |
+-------------------------+
所以你会看到: p3 == p2(地址一样); p1 比 p3 大 4(偏移了一个 int)
这就是多继承下,父类指针会自动偏移到对应子对象位置。
总结:
-
多继承时,子类对象会按继承顺序,把所有父类对象依次放在前面
-
不同父类指针指向同一个子类对象时,地址可能不一样(会自动偏移)
-
但它们都指向同一个对象的不同部分
8. 继承和组合
8.1 继承和组合的区别
|------|--------------------|--------------------|
| 特性 | 继承(public) | 组合 |
| 关系 | is-a(派生类是一个基类) | has-a(类包含其他类对象) |
| 复用方式 | 白箱复用(基类内部细节对派生类可见) | 黑箱复用(被组合对象内部细节不可见) |
| 耦合度 | 高(基类改变影响派生类) | 低(对象间依赖弱) |
| 封装性 | 一定程度破坏封装 | 保持良好封装 |
| 优先选择 | 适合is-a关系或实现多态 | 优先使用,代码维护性好 |
cpp
#include<iostream>
#include<string>
#include<vector>
using namespace std;
// 组合示例:Car has-a Tire
// 语义:有一个
class Tire
{
protected:
string _brand = "Michelin"; // 轮胎品牌
size_t _size = 17; // 轮胎尺寸
};
class Car
{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
// 组合:Car 里面有 4 个 Tire
// 关系:has-a(有一个)
Tire _t1;
Tire _t2;
Tire _t3;
Tire _t4;
};
// 继承示例:BMW is-a Car
// 语义:是一个
class BMW : public Car
{
public:
void Drive() { cout << "好开-操控" << endl; }
};
class Benz : public Car
{
public:
void Drive() { cout << "好坐-舒适" << endl; }
};
// stack 和 vector 的两种实现方式
// 1. 继承方式(不推荐)
// 关系:is-a
// 问题:stack 不该支持 vector 的所有接口(比如 insert、erase)
template<class T>
class stack : public vector<T>
{
// ...
};
// 2. 组合方式(实际 STL 标准推荐)
// 关系:has-a
// 优点:只暴露需要的接口,低耦合
template<class T>
class stack
{
public:
// 栈的接口,底层用 vector 实现
private:
vector<T> _v; // 组合:栈 有一个 vector
};
核心考点:什么时候用继承 / 组合?
- 继承: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 推荐用组合而不是继承?
- 继承会把父类所有接口都继承下来
stack 本来只应该:push、pop、top;如果继承 vector,就会拥有 insert、erase、[] 等不该有的接口;破坏封装,不安全
- 组合低耦合、更安全
只对外暴露栈的接口;底层用 vector 实现,但对外不可见;高内聚、低耦合
总结:is-a 关系 → 用继承;has-a 关系 → 用组合;能用组合就尽量不用继承(组合耦合更低、更安全)
继承是 C++ 面向对象代码复用的核心,理解好权限、隐藏、多继承与虚继承,分清is-a继承和has-a组合的适用场景,才能写出更健壮、易维护的程序。