【C++】继承机制详解
C++ 继承机制详解:从基础到菱形继承
一、为什么需要继承?
在面向对象设计中,当多个类存在大量重复的成员变量和成员函数时,代码冗余问题会变得非常严重。例如,学生(Student)和老师(Teacher)都有姓名、地址、电话、年龄,以及身份认证功能,但他们又各自拥有独特的属性(学号 vs 职称)和行为(学习 vs 授课)。
继承允许我们在保持原有类特性的基础上进行扩展,将公共部分提取到基类(父类)中,让派生类(子类)复用这些成员,从而实现类设计层次的代码复用。
二、继承的基本概念与定义
2.1 语法格式
继承的基本写法如下,基类称为父类,派生类称为子类:
class Student final : public Person
{
\\
};
//Person为Student的父类
2.2 继承方式与访问权限
继承方式有三种:public、protected、private,与类的访问限定符对应。
访问规则的核心逻辑:基类成员在派生类中的最终访问权限,等于 Min(基类中的访问限定符, 继承方式),权限大小关系为 public > protected > private。
关键规则总结:
-
基类
private成员:无论以何种方式继承,在派生类中都是不可见的。注意,不可见不等于没继承——这些成员仍然存在于派生类对象的内存中,只是语法上限制派生类无法访问。 -
protected因继承而生:如果基类成员不想在类外被直接访问,但需要在派生类中访问,就定义为protected。它是专门为继承场景设计的访问级别。 -
默认继承方式:使用
class时默认为private继承;使用struct时默认为public继承。但强烈建议显式写出继承方式,避免歧义。 -
实践建议:实际开发中几乎只使用
public继承。protected和private继承下来的成员只能在派生类内部使用,扩展性和维护性极差,不提倡使用。
2.3 类模板继承的特殊性
当基类是类模板(如 std::vector<T>)时,派生类中访问基类成员需要显式指定类域。
原因在于:模板是按需实例化的。当 stack<int> 实例化时,虽然连带实例化了 vector<int>,但如果派生类中没有直接调用 push_back 等函数,编译器不会实例化这些成员函数,导致找不到标识符的编译错误。
因此,在派生类中访问基类模板成员时,必须写成 vector<T>::push_back(x) 或 this->push_back(x)。
三、基类与派生类间的转换
public 继承体现的是 "is-a" 关系,即派生类对象就是一个基类对象。这种关系带来了天然的赋值兼容性:
3.1 向上转换(安全)
-
派生类对象可以直接赋值给基类对象 / 基类指针 / 基类引用。
-
这个过程被称为切片(Slicing):将派生类中属于基类的那一部分"切"出来,基类指针或引用只指向这部分。
-
注意:对象赋值时发生切片会截断派生类特有的成员;指针和引用则不会发生物理切割,只是访问范围受限。
3.2 向下转换(危险)
-
基类对象不能赋值给派生类对象(编译报错)。
-
基类指针/引用可以通过强制类型转换赋值给派生类指针/引用,但这仅在基类指针确实指向派生类对象时才是安全的。
-
如果基类是多态类型,应使用
dynamic_cast进行运行时安全转换(RTTI)。

class Person {
protected:
string _name = "peter";
string _sex;
int _age = 18;
};
class Student : public Person {
public:
int _No = 42; // 学号
};
int main() {
Student sobj;
// 1. 派生类对象/指针/引用 → 基类(向上转换,安全,自动切片)
Person* pp = &sobj; // 切片:pp 只指向 Person 部分
Person& rp = sobj; // 引用切片
Person pobj = sobj; // 对象切片:_No 被截断!
// 2. 基类 → 派生类(向下转换,不安全)
// sobj = pobj; // 编译报错!
// 3. 强制转换(仅当基类指针确实指向派生类对象时才安全)
Person* pp2 = &sobj;
Student* ps = (Student*)pp2; // 这里安全,因为 pp2 实际指向 Student
return 0;
}
四、继承中的作用域与隐藏
4.1 独立作用域
基类和派生类拥有各自独立的作用域。
4.2 隐藏(Hide)规则
当派生类与基类存在同名成员时,派生类成员会屏蔽基类同名成员的直接访问,这种现象称为隐藏(也叫重定义)。
-
对于成员变量:只要名字相同就构成隐藏。
-
对于成员函数:只需要函数名相同就构成隐藏,与参数列表无关!这意味着基类的
fun()和派生类的fun(int)并不构成重载,而是隐藏关系。
访问被隐藏的基类成员时,需要显式指定作用域:基类::成员名。
工程建议:在继承体系中最好不要定义同名成员,否则极易造成混淆和维护困难。
class Person {
protected:
string _name = "小李子";
int _num = 111; // 身份证号
public:
void fun() { cout << "Person::fun()" << endl; }
};
class Student : public Person {
public:
// 隐藏基类 _num,而非重定义
int _num = 999; // 学号
// 隐藏基类 fun()!不是重载!(函数名相同即隐藏,与参数无关)
void fun(int i) { cout << "Student::fun(int): " << i << endl; }
void Print() {
cout << "姓名: " << _name << endl;
cout << "身份证号: " << Person::_num << endl; // 显式访问被隐藏成员
cout << "学号: " << _num << endl;
}
};
int main() {
Student s;
s.Print();
s.fun(10); // OK:调用 Student::fun(int)
// s.fun(); // 编译错误!基类 fun() 被隐藏,参数不匹配
s.Person::fun(); // OK:显式调用基类版本
return 0;
}
五、派生类的默认成员函数
派生类不主动实现时,编译器会自动生成 6 个默认成员函数。但继承关系使它们的行为变得复杂:
5.1 构造与析构的调用顺序
-
构造:先调用基类构造函数初始化基类部分,再调用派生类构造函数。
-
析构:先调用派生类析构函数清理派生类部分,再自动调用基类析构函数。
这个顺序保证了"先构造的后析构"原则,避免资源泄漏。
5.2 四大默认成员函数的实现要点
-
构造函数:必须在初始化列表中调用基类构造函数。如果基类没有默认构造,必须显式调用。
-
拷贝构造函数:必须在初始化列表中调用基类的拷贝构造,通常传参为派生类对象(切片会自动发生)。
-
赋值运算符
operator=:必须显式调用基类的operator=。由于派生类的operator=隐藏了基类版本,调用时必须加基类作用域限定。 -
析构函数:派生类析构完成后,自动调用基类析构,不需要也不应该在派生类析构中手动调用。
5.3 析构函数的隐藏问题
编译器会对析构函数名做特殊处理,统一改名为 destructor()。因此,如果基类析构函数不加 virtual,基类和派生类的析构函数会构成隐藏关系而非重写。这也是多态章节中要求基类析构函数必须加 virtual 的根本原因。
5.4 如何设计一个不能被继承的类?
-
C++98 方案:将基类的构造函数设为
private。派生类构造时必须调用基类构造,但无法访问私有成员,因此无法实例化。 -
C++11 方案:使用
final关键字修饰基类,简洁直接。
六、继承与友元、静态成员
6.1 友元关系不能继承
基类的友元函数不能访问派生类的私有和保护成员。如果友元函数需要访问派生类私有成员,必须同时成为派生类的友元。
6.2 静态成员唯一共享
如果基类定义了 static 静态成员,那么整个继承体系只存在一份该成员实例。无论派生出多少子类,所有基类和派生类对象共享同一个静态成员。
可以通过 基类::静态成员 或 派生类::静态成员 访问(在公有情况下),但底层地址相同。
七、多继承与菱形继承
7.1 继承模型分类
-
单继承:一个派生类只有一个直接基类。
-
多继承:一个派生类有两个及以上直接基类。对象内存模型中,先继承的基类在前,后继承的在后,派生类成员在最后。
-
菱形继承:多继承的特殊情况。例如
Assistant同时继承Student和Teacher,而Student和Teacher又都继承自Person。

7.2 菱形继承的两大问题
-
二义性:通过
Assistant访问_name时,编译器无法确定是通过Student还是Teacher的路径。必须显式指定a.Student::_name。 -
数据冗余:
Assistant对象中包含两份Person的成员(一份来自Student,一份来自Teacher),造成内存浪费。
7.3 虚继承(virtual inheritance)
使用 virtual public(或 public virtual)继承基类,可以解决菱形继承的数据冗余和二义性问题。
原理:虚继承后,公共基类 Person 在 Assistant 对象中只保留一份,通过虚基表指针(vbptr)间接访问。
构造函数的特殊性:虚继承中,最终的派生类必须直接在初始化列表中调用虚基类的构造函数,中间层的基类对虚基类的构造调用会被忽略。例如 Assistant 的初始化列表中必须直接写 Person(name3),而最终 _name 的值由 Assistant 直接传给 Person 的那个参数决定。
7.4 多继承中的指针偏移
在多继承场景下,将派生类对象地址赋给不同基类指针时,指针值可能不相等。编译器需要进行指针偏移,使基类指针指向对象中对应基类子对象的起始位置。
7.5 标准库中的菱形继承
C++ 标准 IO 库是菱形虚拟继承的典型应用:basic_istream 和 basic_ostream 都虚继承自 basic_ios,而 basic_iostream 多继承自前两者。
更多推荐

所有评论(0)