C++ 继承机制详解
一、继承的概念及定义
1.1 继承的概念
继承(Inheritance)是面向对象编程中代码复用的核心手段。它允许我们在已有类的基础上创建新类,在保持原有特性的同时扩展功能。通过继承构建的层次结构,能让代码更清晰、易维护。
举个例子,如果没有继承,Student 和 Teacher 类都需要重复定义姓名、年龄等成员变量。引入继承后,我们可以将公共部分提取为 Person 基类,让子类复用这些逻辑。
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 定义格式
基类(父类)与派生类(子类)的关系通过继承方式建立。C++ 支持三种继承方式:public、protected 和 private。它们决定了基类成员在派生类中的访问权限变化。
注意:
private和protected在同一个类内作用相似,但在继承时效果截然不同,直接影响派生类的访问能力。
1.2.2 继承基类成员访问方式的变化
| 类成员 / 继承方式 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| 基类的 public 成员 | 派生类的 public 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
| 基类的 protected 成员 | 派生类的 protected 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
| 基类的 private 成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
理解这张表的关键点:
- 基类
private成员在派生类中始终不可见,但物理上仍存在于对象内存中。 - 若希望基类成员不被外部直接访问,但允许派生类访问,应使用
protected。 - 实际开发中,绝大多数情况使用
public继承。protected/private继承会限制成员的可用性,降低扩展性。 - 默认情况下,
class关键字对应private继承,struct对应public继承,建议显式声明。
1.3 继承类模板
容器适配器如 stack 底层通常基于 vector。我们可以尝试用 stack 继承 vector,但这涉及模板实例化的细节。
namespace sxn {
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() {
sxn::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty()) {
cout << st.top() << " ";
st.pop();
}
return 0;
}
为什么需要指定类域?
类模板是按需实例化的。当定义 stack 时,编译器只检查语法。只有当实际使用 stack<int> 并调用 push_back 时,才会实例化 vector<int> 的相关代码。如果未指定作用域,编译器可能找不到 push_back 标识符,导致编译错误。
二、基类和派生类间的转化
- 派生类对象 -> 基类指针/引用:这是安全的,称为'切片'或'切割'。基类指针指向的是派生类对象中属于基类的那部分数据。
- 基类对象 -> 派生类对象:不允许直接赋值,因为派生类包含更多成员。
- 强制转换:基类指针可转为派生类指针,但必须确保该指针实际指向的是派生类对象。对于多态类型,推荐使用 RTTI 的
dynamic_cast进行安全转换。

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;
// 2. 基类对象不能赋值给派生类对象,这里会编译报错
// sobj = pobj;
return 0;
}
三、继承中的作用域
3.1 隐藏规则
- 基类和派生类拥有独立的作用域。
- 若两者存在同名成员,派生类成员将屏蔽基类同名成员的直接访问,这叫隐藏。
- 函数名相同即构成隐藏(区别于重载)。在派生类中可通过
基类::成员名显式访问被隐藏的基类成员。 - 实际开发中,尽量避免在继承体系中定义同名成员,以免混淆。
// Student 的_num 和 Person 的_num 构成隐藏关系
class Person {
protected:
string _name = "小雷子"; // 姓名
int _num = 072; // 身份证号
};
class Student : public Person {
public:
void Print() {
cout << "姓名:" << _name << endl;
cout << "身份证号:" << Person::_num << endl; // 显式访问基类
cout << "学号:" << _num << endl;
}
protected:
int _num = 520; // 学号
};
int main() {
Student s1;
s1.Print();
return 0;
}
3.2 考察继承作用域相关选择题
问题: 下面 A 和 B 类中的两个 func 构成什么关系? A. 重载 B. 隐藏 C. 没关系
解析: 重载发生在同一作用域内。基类和派生类是两个不同作用域,且函数名相同,因此是隐藏关系。
问题: 下面程序的编译运行结果是什么?
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); // 调用派生类 fun(int)
b.fun(); // 试图调用基类 fun(),但被隐藏
return 0;
};
答案: 编译报错。虽然 b.fun() 语义上想调用基类函数,但由于派生类定义了同名的 fun,基类版本被隐藏。编译器在语义分析时发现调用不匹配,直接报错。
四、派生类的默认成员函数
派生类会自动生成 6 个默认成员函数,其行为如下:
- 构造函数:必须调用基类构造函数初始化基类部分。若基类无默认构造,需在初始化列表中显式调用。
- 拷贝构造函数:需调用基类拷贝构造完成基类部分的拷贝。
- 赋值运算符:需调用基类赋值运算符。注意派生类
operator=可能隐藏基类版本,需指定作用域调用。 - 析构函数:先执行派生类析构,再自动调用基类析构,确保清理顺序正确。
- 初始化顺序:先基类构造,后派生类构造;析构则相反。
- 虚析构:在多态场景下,基类析构函数建议加
virtual,否则派生类析构可能与基类构成隐藏而非重写。
示例演示:
class Person {
public:
Person(const char* name = "joke") : _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) : Person(name), _num(num) {
cout << "Student()" << endl;
}
Student(const Student& s) : Person(s), _num(s._num) {
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;
}
return *this;
}
~Student() {
cout << "~Student()" << endl;
}
protected:
int _num; // 学号
};
int main() {
Student s1("jack", 18);
Student s2(s1);
Student s3("rose", 17);
s1 = s3;
return 0;
}
4.2 实现一个不能被继承的类
方法 1:将基类构造函数设为私有,派生类无法访问。
方法 2:C++11 引入 final 关键字修饰基类。
class Base final {
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};
class Derive : public Base { // 编译错误:Derive 不能继承 final 类
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
五、继承与友元
友元关系不能继承。基类的友元无法访问派生类的私有和保护成员。
#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 {
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s) {
cout << p._name << endl;
cout << s._stuNum << endl; // 错误:Display 不是 Student 的友元
}
int main() {
Person p;
Student s;
Display(p, s);
return 0;
}
六、继承与静态成员
基类定义的 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 << endl;
cout << &s._name << endl;
cout << &p._count << endl;
cout << &s._count << endl;
cout << Person::_count << endl;
cout << Student::_count << endl;
return 0;
}
七、多继承及菱形继承问题
7.1 继承模型
- 单继承:一个派生类只有一个直接基类。
- 多继承:一个派生类有两个或以上直接基类。内存布局通常是先继承的基类在前,派生类成员在最后。
- 菱形继承:多继承的特殊情况,会导致数据冗余和二义性。
例如 Assistant 同时继承 Student 和 Teacher,而两者都继承自 Person。此时 Assistant 对象中会有两份 Person 的成员。
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._name = "peter"; // 编译报错:对_name 的访问不明确
return 0;
}
解决二义性需显式指定:a.Student::_name = "xxx";,但这无法解决数据冗余。
7.2 虚继承
引入 virtual 关键字可解决菱形继承问题,确保基类在派生类中只有一份副本。
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 = "bob"; // 正常访问,无二义性
return 0;
}
注意:虚继承会增加底层实现的复杂度及性能开销,除非必要,不建议随意设计菱形继承结构。
7.3 多继承中指针偏移问题
class Base1 {
public:
int _b1;
};
class Base2 {
public:
int _b2;
};
class Derive : public Base1, public Base2 {
public:
int _d;
};
int main() {
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
// 答案:p1 == p3 != p2
return 0;
}
八、继承和组合
- 继承是
is-a关系,派生类对象也是基类对象。属于白箱复用,耦合度高。 - 组合是
has-a关系,对象中包含另一个对象。属于黑箱复用,耦合度低。 - 原则:优先使用组合,而非继承。组合能更好地封装内部细节,降低依赖。当然,若确实需要多态或明确的
is-a关系,继承仍是必要的。
就像 stack 继承 vector 一样,标准库也提供了组合的实现方式供选择。
总结
继承为后续学习多态打下了坚实基础,是深入理解 C++ 面向对象编程的关键一步。合理运用继承机制,能够让我们设计出更加优雅、可扩展的代码结构。在实际开发中,要注意避免过度继承带来的复杂性和维护成本,优先考虑组合模式。


