跳到主要内容
C++ 继承机制详解:概念、作用域与虚继承 | 极客日志
C++ 算法
C++ 继承机制详解:概念、作用域与虚继承 C++ 继承是面向对象编程实现代码复用的关键机制。详细阐述了继承的概念定义、基类与派生类的转换规则及访问权限变化。重点讲解了派生类默认成员函数的生成逻辑、作用域隐藏现象以及友元与静态成员的继承特性。针对多继承带来的数据冗余和二义性问题,深入剖析了菱形继承模型及其虚继承解决方案。最后对比了继承与组合的适用场景,强调优先使用组合以降低耦合度,为构建高质量 C++ 类层次结构提供实践指导。
不羁 发布于 2026/3/29 更新于 2026/4/25 1 浏览C++ 继承机制详解
1. 继承的概念及定义
1.1 继承的概念
继承(inheritance)是面向对象程序设计中实现代码复用的核心手段。它允许我们在保持原有类特征的基础上进行扩展,增加新的方法(成员函数)和属性(成员变量),从而产生新的类,称为派生类。继承构建了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。相比于函数层次的复用,继承实现了类设计层次的复用。
假设我们之前设计了 Student 和 Teacher 两个类,它们都包含姓名、地址、电话、年龄等公共信息,以及身份认证功能。如果分别定义,会导致大量冗余。虽然老师有职称、学生有学号等独有成员,但公共部分完全可以提取出来。
class Student {
public :
void identity () { }
void study () { }
protected :
string _name = "jack" ;
string _address;
string _tel;
int _age = 19 ;
int _num;
};
class Teacher {
public :
void identity () { }
void teaching () { }
protected :
string _name = "张三" ;
int _age = 18 ;
string _address;
string _tel;
string _title;
};
我们可以将公共成员提取到 Person 基类中,让 Student 和 Teacher 继承 Person,这样既减少了重复定义,又提升了代码的简洁性和可维护性。
class Person {
public :
void identity () {
cout << << _name << endl;
}
:
string _name = ;
string _address;
string _tel;
_age = ;
};
: Person {
:
{ }
:
_num;
};
: Person {
:
{ }
:
string title;
};
{
Student s;
Teacher t;
s. ();
t. ();
;
}
"void identity()"
protected
"张三"
int
19
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 定义格式 在继承关系中,被继承的类称为基类(父类),继承后的类称为派生类(子类)。
1.2.2 继承类成员访问方式的变化 继承方式决定了基类成员在派生类中的访问权限变化。下表总结了不同继承方式下的权限映射:
类成员/继承方式 public 继承 protected 继承 private 继承 基类的 public 成员 派生类的 public 成员 派生类的 protected 成员 派生类的 private 成员 基类的 protected 成员 派生类的 protected 成员 派生类的 protected 成员 派生类的 private 成员 基类的 private 成员 在派生类中不可见 在派生类中不可见 在派生类中不可见
基类的 private 成员在任何继承方式下对派生类都是不可见的。这并不意味着它们没有被继承,而是语法上限制了派生类对象无法访问它们。
如果希望某些成员在类外不可访问,但在派生类中可访问,应使用 protected。保护成员限定符正是为继承场景设计的。
通过表格可以发现,基类其他成员在派生类的访问权限遵循最小原则:Min(成员在基类的访问限定符,继承方式),即 public > protected > private。
使用关键字 class 时默认继承方式是 private,使用 struct 时默认是 public。为了代码清晰,建议显式写出继承方式。
实际开发中通常使用 public 继承,很少使用 protected 或 private 继承,因为后两者会限制成员的可见性,不利于扩展和维护。
class Person {
public :
void Print () { cout << _name << endl; }
protected :
string _name;
private :
int _age;
};
class Student : public Person {
protected :
int _num;
};
1.3 继承类模板 当基类是类模板时,派生类需要指定类域,否则编译可能报错找不到标识符。这是因为模板按需实例化,若未实例化的成员函数被调用,编译器无法解析。
namespace Achieve {
template <class T >
class stack : public 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 () {
Achieve::stack<int > st;
st.push (1 );
st.push (2 );
st.push (3 );
while (!st.empty ()) {
cout << st.top () << " " ;
st.pop ();
}
return 0 ;
}
2. 基类和派生类间的转换
public 继承的派生类对象可以赋值给基类的指针或引用。这被称为向上转型(Upcasting),有时也称为切片(Slicing),指把派生类中基类那部分切出来,基类指针或引用指向的是派生类对象中属于基类的那部分数据。
基类对象不能直接赋值给派生类对象。
基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用。但这必须是安全的,即基类指针确实指向派生类对象。如果涉及多态模型,建议使用 RTTI(Run-Time Type Information)中的 dynamic_cast 进行安全识别后再转换。
class Person {
protected :
string _name;
string _sex;
int _age;
};
class Student : public Person {
public :
int _num;
};
int main () {
Student sobj;
Person* pp = &sobj;
Person& rp = sobj;
Person pobj = sobj;
return 0 ;
}
3. 继承中的作用域
3.1 隐藏规则
在继承体系中,父类和子类都有独立的作用域。
如果子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏。在子类成员函数中,可以使用 父类::子类成员 显式访问。
对于成员函数的隐藏,只要函数名相同就构成隐藏,参数列表不影响隐藏判定。
实际开发中,继承体系里最好避免定义同名的成员,以免造成混淆。
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 = 111 ;
};
int main () {
Student s;
s.Print ();
return 0 ;
}
3.2 考察继承作用域相关选择题 题目 1: A 和 B 类中的两个 func 构成什么关系?
A. 重载 B. 隐藏 C. 没关系
答案:B (函数名相同,参数不同,在继承中视为隐藏而非重载)
class A {
public :
void fun () { cout << "func()" << endl; }
};
class B : public A {
public :
void fun (int i) { cout << "func(int i)" << endl; }
};
int main () {
B b;
b.fun (10 );
b.fun ();
return 0 ;
}
4. 派生类的默认成员函数 C++ 有 6 个默认成员函数,如果不写,编译器会自动生成。在派生类中,这些函数的生成逻辑如下:
构造函数 :派生类的构造函数必须调用基类的构造函数来初始化基类那一部分成员。若基类没有默认构造函数,则必须在派生类构造函数的初始化列表中显式调用。
拷贝构造函数 :派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类的拷贝初始化。
赋值运算符 operator= :派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。需要注意的是,派生类的 operator= 会隐藏基类的 operator=,所以显式调用基类的 operator= 时,需要指定基类作用域。
析构函数 :派生类的析构函数在被调用完成后会自动调用基类的析构函数清理基类成员。这是为了保证派生类对象先清理自身成员,再清理基类成员的顺序。
初始化顺序 :派生类对象初始化时,先调用基类构造,再调用派生类构造。
析构顺序 :派生类对象析构清理时,先调用派生类析构,再调用基类析构。
析构函数重写 :在多态场景中,析构函数通常需要构成重写。重写的条件之一是函数名相同。编译器会对析构函数名进行特殊处理,处理成 destructor(),所以基类析构函数不加 virtual 的情况下,派生类析构函数和基类析构函数构成隐藏关系。
class Person {
public :
Person (const string s = "张三" ) :_name(s) {}
Person (const Person& p) :_name(p._name) {}
Person& operator =(const Person& p) {
if (this != &p) {
_name = p._name;
}
return *this ;
}
protected :
string _name;
};
class Student : public Person {
public :
Student (const string& p, int num) : Person (p), _num(num) {}
Student (const Student& s) : Person (s._name), _num(s._num) {}
Student& operator =(const Student& s) {
if (this != &s) {
Person::operator =(s);
_num = s._num;
}
return *this ;
}
private :
int _num = 1 ;
};
4.2 实现一个不能被继承的类
将基类的构造函数私有化。派生类的构造必须调用基类的构造函数,但基类的构造函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。
C++11 新增了一个 final 关键字,修饰基类,派生类就不能继承了。
class Base final {
public :
void func () { cout << "Base::func()" << endl; }
protected :
int a = 1 ;
private :
};
class Derive : public Base {
public :
void func () { cout << "Derive::fun()" << endl; }
protected :
int b = 2 ;
};
int main () {
Base b;
return 0 ;
}
5. 继承与友元 友元关系不能继承。也就是说,基类的友元不能访问派生类的私有和保护成员。
class Student ;
class Person {
friend void Display (const Person& p, const Student& s) ;
protected :
string _name;
};
class Student : public Person {
protected :
string _stuname;
};
void Display (const Person& p, const Student& s) {
cout << p._name << endl;
cout << s._stuname << endl;
}
int main () {
Person p;
Student s;
return 0 ;
}
6. 继承与静态函数 若基类定义了 static 成员,则整个继承体系中只有一个这样的成员。无论派生出多少派生类,都只有一个 static 成员实例。
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. 多继承及其菱形继承问题
7.1 继承模型
单继承 :一个派生类只有一个直接基类。
多继承 :一个派生类有两个或以上直接基类。多继承对象在内存中的模型是,先继承的基类在前面,后继承的基类在后面,派生类成员放在最后面。
菱形继承 :菱形继承是多继承的一种特殊情况。从对象成员模型构造可以看出,菱形继承存在数据冗余和二义性 的问题。例如在 Assistant 的对象中,Person 成员会有两份。支持多继承就一定会有菱形继承的风险。像 Java 就不直接支持多继承,规避掉了这里的问题,所以实践中不建议设计出菱形继承这样的模型。
7.2 虚继承 C++ 的语法复杂,多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承。底层实现很复杂,性能也会有一些损失,所以最好不要设计出菱形继承。多继承可以认为是 C++ 的特性之一,后来的一些编程语言都没有多继承,如 Java。
解决菱形继承问题的方法是使用虚继承 (Virtual Inheritance)。
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;
};
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 ;
}
在虚继承中,无论是使用还是底层实现都会变得复杂很多。虽然有多继承语法支持,就一定会存在设计出菱形继承的风险。像 Java 是不支持多继承的,就避开了菱形继承。
注意虚继承构造函数的调用顺序:最远基类(如 Person)的构造函数会在所有中间基类之前被调用。
class Person {
public :
Person (const string& name) :_name(name) {}
protected :
string _name;
};
class Student : virtual public Person {
public :
Student (const string& name, int num) : Person (name), _num(num) {}
protected :
int _num;
};
class Teacher : virtual public Person {
public :
Teacher (const string& name, const int id) : Person (name), _id(id) {}
protected :
int _id;
};
class Assistant : public Student, public Teacher {
public :
Assistant (const string name1, const string name2, const string name3)
: Person (name3), Student (name1, 1 ), Teacher (name2, 2 ) {}
protected :
string _majorCourse;
};
int main () {
Assistant a ("张三" , "李四" , "王五" ) ;
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;
return 0 ;
}
7.4 IO 库中的菱形虚拟继承 标准库中 basic_ostream 和 basic_istream 都虚继承自 basic_ios,以避免数据冗余。
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. 继承和组合
8.1 继承和组合的区别
public 继承 是一种 is-a 的关系。即每种派生类对象都是一个基类对象。
组合 是一种 has-a 的关系。假设 B 组合了 A,每个 B 对象中都有一个 A 对象。
继承 允许你根据基类的实现来派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语'白箱'是相对可视化而言:在继承方式中,基类的内部细节对派生类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大影响。派生类和基类间的依赖关系很强,耦合度很高。
对象组合 是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以'黑箱'的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于我们保持每个类被封装。
结论 :优先使用组合,而不是继承。尽量多用组合,组合的耦合度低,代码维护性好。不过也不太绝对,类之间的关系适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合用组合(has-a),就用组合。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
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