C++ 继承机制详解
继承(inheritance)是面向对象程序设计中代码复用的核心手段。它允许我们在保持原有类特征的基础上进行扩展,增加方法或属性,从而产生新的类——派生类。这种机制体现了由简单到复杂的认知过程,实现了类设计层次的复用。
1. 继承的概念及定义
1.1 概念理解
在引入继承之前,如果我们设计 Student 和 Teacher 两个类,它们都包含姓名、地址、电话等公共成员,以及各自独有的身份认证逻辑。直接复制这些公共成员会导致代码冗余。
通过提取公共部分为 Person 基类,让 Student 和 Teacher 继承自 Person,即可复用这些成员,大幅提升代码的简洁性和可维护性。
class Person {
public:
void identity() { cout << "void identity()" << _name << endl; }
protected:
string _name = "张三";
string _address;
string _tel;
int _age = 19;
};
class Student : public Person {
public:
void study() {}
protected:
int _num; // 学号
};
class Teacher : public Person {
public:
void teaching() {}
protected:
string title;
};
int main() {
Student s;
Teacher t;
s.identity();
t.identity();
return 0;
}
1.2 继承定义与访问权限
基类(父类)的成员在派生类中的可见性取决于继承方式。下表总结了不同继承方式下成员访问权限的变化:
| 类成员/继承方式 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| 基类的 public 成员 | 派生类的 public 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
| 基类的 protected 成员 | 派生类的 protected 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
| 基类的 private 成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
几点关键说明:
- 私有成员不可见:基类的
private成员无论以何种方式继承,在派生类中都是不可见的。这并不意味着数据不存在于对象中,而是语法上限制了访问。 - 保护成员的作用:如果希望基类成员不在类外被访问,但需要在派生类中访问,应定义为
protected。 - 默认继承方式:使用关键字
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 未实例化时找不到
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);
while (!st.empty()) {
cout << st.top() << " ";
st.pop();
}
return 0;
}
2. 基类和派生类间的转换
- 向上转型:
public继承的派生类对象可以赋值给基类的指针或引用。这被称为'切片',即只保留派生类中基类的那部分。 - 向下转换:基类对象不能直接赋值给派生类对象。基类指针或引用可以通过强制类型转换指向派生类对象,但需确保基类指针实际指向的是派生类对象。若涉及多态模型,建议使用 RTTI(如
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; // 切片操作
// sobj = pobj; // 编译报错:父类对象不能赋值给派生类对象
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; // 隐藏了 Person::_num
};
int main() {
Student s;
s.Print();
return 0;
}
3.2 常见考点
- 重载 vs 隐藏:若子类函数名与父类不同但参数不同,通常构成重载;若仅函数名相同,则构成隐藏。
- 调用解析:调用
b.fun()时,若子类定义了同名函数,优先调用子类的版本,除非显式指定作用域。
4. 派生类的默认成员函数
编译器会自动生成默认成员函数,但在继承场景下,其行为有特殊规则:
- 构造函数:派生类构造函数必须调用基类构造函数初始化基类部分。若基类无默认构造函数,必须在初始化列表中显式调用。
- 拷贝构造:派生类拷贝构造需调用基类拷贝构造完成基类部分的拷贝。
- 赋值运算符:派生类
operator=会隐藏基类的operator=。若需调用基类版本,需指定基类作用域。 - 析构函数:派生类析构完成后自动调用基类析构。清理顺序遵循'先派生后基类'的原则,确保资源正确释放。
- 虚析构:在多态场景中,基类析构函数应声明为
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;
};
// class Derive : public Base { ... }; // 编译报错
5. 继承与友元
友元关系不具备继承性。基类的友元不能访问派生类的私有和保护成员。若需访问,需在派生类中重新声明友元。
6. 继承与静态函数
静态成员在整个继承体系中只有一个实例。无论派生出多少类,所有类共享同一份静态成员数据。
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;
return 0;
}
7. 多继承及其菱形继承问题
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"; // 二义性错误
// 解决二义性需指定作用域,但无法解决数据冗余
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
7.2 虚继承
为解决菱形继承的数据冗余问题,可使用 virtual 关键字进行虚继承。底层实现较复杂,性能有损耗,因此实践中不建议过度设计菱形继承结构。
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, int id) : Person(name), _id(id) {}
protected:
int _id;
};
class Assistant : public Student, public Teacher {
public:
Assistant(const string& n1, const string& n2, const string& n3)
: Person(n3), Student(n1, 1), Teacher(n2, 2) {}
protected:
string _majorCourse;
};
int main() {
Assistant a("张三", "李四", "王五");
return 0;
}
7.3 多继承指针偏移
在多继承中,不同基类指针的地址可能不同。例如 Derive 继承 Base1 和 Base2,Base1* 和 Base2* 指向同一对象时,其内部偏移量不同。
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;
}
8. 继承和组合
- 继承:是一种 is-a 的关系。派生类对象本质上也是基类对象。属于白箱复用,耦合度高,基类改变会影响派生类。
- 组合:是一种 has-a 的关系。新对象包含另一个对象作为成员。属于黑箱复用,耦合度低,接口定义良好。
设计原则:优先使用组合而非继承。组合能更好地封装细节,降低依赖。仅在明确满足 is-a 关系或需要实现多态时才考虑继承。

