1. 继承的概念和定义
概念
继承(Inheritance)是面向对象程序设计中代码复用的重要手段。它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),从而产生新的类,称为派生类。继承体现了由简单到复杂的认知过程,实现了类设计层次的复用。
假设我们有一个学生和一个教师类,它们都有公共的成员如名字、电话、地址等。如果不在基类中封装这些公共属性,就会导致代码冗余。通过继承,我们可以将公共成员封装到一个 Person 类中,Student 和 Teacher 继承 Person,即可复用这些成员。
class Person {
public:
// 身份识别,比如进入图书馆打卡
void Identify() {
cout << "void Identify()" << _name << endl;
}
protected:
string _name; // 名字
string _tel; // 电话
string _address;// 地址
int _age; // 年龄
};
class Student : public Person {
public:
void study() { /* ... */ }
protected:
int _stuid; // 学号 <- 学生特有
};
class Teacher : public Person {
public:
void teaching() { /* ... */ }
protected:
string _title; // 职称 <- 教师特有
};
定义
在 C++ 中,Person 被称为基类(父类),Student 和 Teacher 被称为派生类(子类)。
C++ 支持三种继承方式:public、protected、private。不同的继承方式会影响基类成员在派生类中的访问权限。
- public 继承:基类的 public/protected 成员在派生类中保持原有的访问权限。
- protected 继承:基类的 public/protected 成员在派生类中变为 protected。
- private 继承:基类的所有成员在派生类中均变为 private。
注意:基类的 private 成员无论以什么方式继承都是不可见的(虽然物理上存在于对象内存中,但语法上无法访问)。使用关键字 class 时默认继承方式是 private,使用 struct 时默认为 public,建议显式写出继承方式。实际开发中一般使用 public 继承。
继承类模板示例
当基类是模板类时,子类访问基类成员需要指定访问限定符,因为模板类是按需实例化。
template<class T>
class Stack : public vector<T> {
public:
void push(const T& x) {
// 必须指定作用域,否则找不到 push_back
vector<T>::push_back(x);
}
void pop() {
vector<T>::pop_back();
}
};
2. 基类和派生类间的转换
public 继承的派生类对象可以赋值给基类的指针或引用。这通常被称为'切片'(Slicing)现象。寓意是把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。
基类对象不能直接赋值给派生类对象。基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用,但必须是基类指针确实指向派生类对象时才安全。若基类是多态类型,可使用 RTTI(Run-Time Type Information)的 dynamic_cast 进行安全转换。
class Person {
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person {
public:
int _No; // 学号
};
int main() {
Student s;
// 子类切片给父类的指针或者引用
Person* ptr = &s;
Person& a = s;
// 派生类对象可以赋值给基类的对象是通过调用基类的拷贝构造完成的
Person per = s;
// 父类对象不可以赋值给子类对象,编译会报错
// s = per;
return 0;
}
3. 继承中的作用域
隐藏规则
在继承体系中,基类和派生类都有独立的作用域。派生类和基类中有同名成员时,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。
在派生类成员函数中,可以使用 基类::基类成员 显式访问被隐藏的基类成员。需要注意的是,如果是成员函数的隐藏,只需要函数名相同就构成隐藏。在实际开发中,继承体系里最好不要定义同名的成员,以免混淆。
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 = 999; // 学号
};
int main() {
Student s1;
s1.Print();
return 0;
}
4. 派生类的默认成员函数
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认构造函数,则必须在派生类构造函数的初始化列表中显式调用。
- 拷贝构造函数:派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- operator=:派生类的
operator=必须要调用基类的operator=完成基类的复制。需要注意派生类的operator=隐藏了基类的operator=,所以显式调用基类的operator=需要指定基类作用域。 - 析构函数:派生类的析构函数在被调用完成后会自动调用基类的析构函数清理基类成员。顺序是先清理派生类成员,再清理基类成员(满足先定义的类后析构的设计要求)。
class Person {
public:
Person(const char* name = "peter") : _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;
}
实现一个不能被继承的类
方法一:将基类的构造函数私有化,派生类无法访问基类构造函数,从而无法实例化。
方法二:C++11 新增了 final 关键字,修饰基类后,派生类就不能继承了。
class Base final {
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};
// class Derive : public Base { ... }; // 编译错误
5. 继承与友元
友元关系不能继承。也就是说,基类友元不能访问派生类私有和保护成员,但该友元依旧可以访问基类的私有和保护成员。
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 的友元
}
6. 继承与静态成员
基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个派生类,都只有一个 static 成员实例。即基类实例化的对象和派生类实例化的对象调用 static 成员时,调用的是同一个。
7. 多继承及其菱形继承问题
继承模型
- 单继承:一个派生类只有一个直接基类。
- 多继承:一个派生类有两个或以上直接基类。多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员放在最后面。
- 菱形继承:菱形继承是多继承的一种特殊情况。从对象成员模型可以看出,菱形继承有数据冗余和二义性的问题。
例如在 Assistant 的对象中,Person 成员会有两份。Assistant 实例化的对象访问 Person 类中的成员时,不知道访问的是由 Student 类继承下来的 Person 还是 Teacher 继承下来的 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;
// 编译报错:对"_name"的访问不明确
// a._name = "peter";
// 需要显式指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
虚继承
使用虚继承可以解决菱形继承的数据冗余和访问时的二义性。
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;
}
在实际开发中,菱形继承足够复杂时会出现各种问题。实践中不建议设计出菱形继承这样的模型,或者说就不要设计菱形继承。但在 C++ IO 库中可以看到虚继承的应用,例如 basic_ostream 和 basic_istream 都虚继承自 basic_ios。
8. 继承和组合
- public 继承是一种
is-a的关系。每个派生类对象都是一个基类对象。继承允许你根据基类的实现来定义派生类的实现,通常被称为白箱复用。继承一定程度破坏了基类的封装,基类的改变对派生类有很大影响,耦合度高。 - 组合是一种
has-a的关系。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口,这种复用风格被称为黑箱复用。组合类之间没有很强的依赖关系,耦合度低。
程序设计要求高内聚、低耦合。因此优先使用组合,而不是继承。实际尽量多用组合,组合的耦合度低,代码维护性好。不过也不太绝对,类之间的关系适合继承(is-a)那就用继承,另外要实现多态也必须要继承。类之间的关系既适合用继承也适合组合,就用组合。
本文梳理了 C++ 继承的核心知识点,包括访问权限控制、对象切片、默认成员函数调用顺序以及多继承带来的菱形问题解决方案。理解这些机制有助于编写更安全、可维护的 C++ 代码。

