一、概念
继承是类设计层次复用的核心手段。通过继承,子类可以复用基类的成员,同时扩展新功能。
语法示例:
class Person { /* 基类 */ };
class Student : public Person { /* 派生类 */ };
继承关系与访问限定符紧密相关。继承后,基类成员的可见性会发生变化:
- 私有成员:虽然被继承到了派生类对象中,但语法上限制派生类(无论内部还是外部)都无法直接访问。它们在基类内可用,在基类外不可见。
- 保护成员:如果希望基类成员不在类外直接访问,但在派生类中可访问,应定义为
protected。 - 访问权限计算:基类其他成员在子类的访问方式遵循规则:Min(成员在基类的访问限定符,继承方式)。即
public > protected > private。
二、基类和派生类对象赋值转换
不同类型对象赋值需要类型转换,但继承体系中有特殊规则。
父类对象不能赋值给子类对象,强转也不行。否则子类特有的成员变量怎么办?给随机值吗?
Person p; Student s;
s = p; // 错误:父类不能赋值给子类
// s = (Student)p; // 强转也不行
p = s; // 正确
子类对象可以赋值给父类的对象、指针、引用。 当子类赋值给父类对象时,语法上特殊处理,没有发生类型转换,而是赋值兼容(切片)。把子类里属于父类的那部分切出来,拷贝给父类。
怎么证明没有发生类型转换?可以用别名验证。如果把父类的成员变量放成 public,可以看到 p2 变成了子类中父类那一部分的别名,而非整个子类对象。
三、继承中的作用域
作用域查找是编译时的规则。定义了一个类就有类域,子类和父类有独立的类域。
同名成员隐藏:子类成员将屏蔽父类对同名成员的直接访问,这叫隐藏(重定义)。在子类成员函数中,可以使用 父类::父类成员 显式访问。
现实中尽量不要在继承体系里定义同名成员,这容易造成混淆。
class Person {
public:
void fun() { cout << "Person::func()" << endl; }
protected:
string _name = "小李子";
int _num = 111;
};
class Student : public Person {
public:
void fun(int) { cout << "Student::func(int)" << endl; } // 隐藏了父类的 fun()
void Print() {
cout << "姓名:" << _name << endl;
cout << _num << endl; // 999,子类成员
cout << Person::_num << endl; // 111,显式访问父类
}
protected:
int _num = 999;
};
int main() {
Student s;
s.Print();
s.fun(1); // 调用子类版本
s.Person::fun(); // 显式调用父类版本
return 0;
}
注意:同一作用域才能构成重载。这里两个 fun 只是隐藏关系,不是重载。
四、派生类的默认成员函数
设计理念:父干父的活,子干子的活。
1. 构造函数
子类必须调父类的构造函数来初始化父类成员。默认情况下,在初始化列表自动调用父类的默认构造函数。若父类不提供默认构造函数,则必须手动指定。
规定:子类的构造函数中,不能在初始化列表显示初始化父类成员(因为那是基类部分),只能在构造函数体内初始化,或者在初始化列表中调用基类构造函数。
class Person {
public:
Person(const char* name) : _name(name) { cout << "Person()" << endl; }
~Person() { cout << "~Person()" << endl; }
protected:
string _name;
};
class Student : public Person {
public:
// 必须在初始化列表中调用基类构造函数
Student(const char* name, int id) : Person(name), _id(id) {}
protected:
int _id;
};
调试发现,在初始化列表先走 Person(name),说明继承的成员声明在自己的成员之前。初始化顺序是按声明顺序来的。
2. 拷贝构造
必须调父类的拷贝构造。子类对象可以传给父类对象的指针/引用,所以可以直接传子类对象给父类拷贝构造。
class Student : public Person {
public:
Student(const Student& s) : Person(s), _id(s._id) {}
// ... 省略其他代码
};
如果删去父类拷贝构造调用,默认不会调拷贝构造函数,而是调默认构造函数,这会导致数据不一致。
3. 赋值重载
class Student : public Person {
public:
Student& operator=(const Student& s) {
if (this != &s) {
Person::operator=(s); // 显式调用父类赋值,避免隐藏
_id = s._id;
}
return *this;
}
};
4. 析构函数
构造、拷贝、赋值要显示调用。析构函数是个特殊,为保证析构顺序,会自动调。
为什么不能手动调用父类析构?
由于多态的原因,析构函数的函数名被特殊处理了。如果手动调用 Person::~Person(),可能会导致重复析构或顺序错误。总共 2 个对象,调了 4 次析构函数就是典型的错误示范。
正确的做法:子类析构函数完成后,系统自动调用父类析构,这样就保证了先子后父的析构顺序。为什么要保证先子后父?因为子类中有可能用到父类成员,父类不可能用到子类。如果先析构父类,子类再访问父类成员就会出错。
五、继承与友元
友元关系不能继承。父类友元不能访问子类私有和保护成员。
class Student;
class Person {
friend void Display(const Person& p, const Student& s);
protected:
string _name;
};
class Student : public Person {
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum;
};
void Display(const Person& p, const Student& s) {
cout << p._name << endl;
cout << s._stuNum << endl; // 需要 Student 也声明友元
}
屏蔽掉 Student 中的友元声明会报错,因为友元关系不具备传递性。
六、继承与静态成员
父类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个 static 成员实例,在子类中不会单独拷贝一份。
class Person {
public:
Person() { ++_count; }
static int _count;
};
int Person::_count = 0;
class Student : public Person {};
// ... 使用逻辑同上
七、多继承、菱形继承、菱形虚拟继承
实践中可以谨慎用多继承,不要用菱形继承。
单继承:一个子类只有一个直接父类。 多继承:一个子类有两个或以上直接父类。 菱形继承:是多继承的一种特殊情况,会导致数据冗余和二义性。
class Person { public: string _name; int _age; };
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 as;
as._age = 19; // 报错:对'_age'的访问不明确
return 0;
}
二义性可以通过指定访问路径解决(如 as.Student::_age),但这违背常理且维护困难。
虚拟继承解决了数据冗余和二义性问题。
class Student : virtual public Person { ... };
class Teacher : virtual public Person { ... };
这样设计后,Assistant 中只有一份 Person 的副本。原理是通过虚基表(vptr/vtable)机制,先取到偏移量,计算 _age 在对象中的地址,再访问。
八、继承和组合
class C {};
class D : public C {}; // 继承
class E { private: C _cc; }; // 组合
继承:白箱复用,父类的内部细节对子类可见;耦合度高。改变父类的成员,子类要跟着改。 组合:黑箱复用,对象的内部细节不可见;耦合度低。
实践中多用组合。继承和组合都可以,就用组合;更适合用继承的用继承;实现多态,必须用继承。
- public 继承是 is-a 的关系:每个子类对象一定是父类对象(如教师、学生都是人)。
- 组合是 has-a 的关系:E 组合了 C,每个 E 对象中都有一个 C 对象(如车有轮胎)。
以上就是 C++ 继承机制的核心内容。理解这些概念对于编写健壮、可扩展的代码至关重要。

