C++ 继承机制详解
一、概念与语法
继承是类设计层次复用的核心手段。在 C++ 中,基类(父类)定义了通用特性,派生类(子类)在此基础上扩展。
基本语法:
class Person { /* ... */ };
class Student : public Person { /* ... */ };
继承关系决定了访问权限的传递规则。基类的私有成员虽然被继承到了派生类对象中,但在语法层面不可见。这意味着无论你在派生类内部还是外部,都无法直接访问基类的私有成员。如果希望某些成员在基类外不可见,但允许派生类访问,应将其定义为 protected。
访问权限遵循以下原则:基类成员在子类的访问方式取决于 Min(成员在基类的访问限定符,继承方式)。即 public > protected > private。
二、对象赋值与类型转换
不同类型对象赋值时涉及类型转换,但在继承体系中有一些特殊规则。
- 父类不能赋值给子类:即使强转也不行。因为父类对象不包含子类特有的成员变量,强行赋值会导致数据丢失或随机值。
- 子类可以赋值给父类:这是赋值兼容(切片/Slicing)。将子类对象中属于父类的部分拷贝给父类对象,子类特有部分会被丢弃。
- 指针与引用:子类对象可以赋值给父类的指针或引用,这不会发生切片,而是指向同一块内存区域的不同视角。
class Person {
public:
void Print() { cout << "name:" << _name << endl; }
protected:
string _name = "peter";
int _age = 18;
};
class Student : public Person {
protected:
int _stuid;
};
int main() {
Person p;
Student s;
// s = p; // 错误:父类不能赋值给子类
p = s; // 正确:发生切片,只拷贝 Person 部分
return 0;
}
三、作用域与隐藏
继承体系中存在独立的类域。当子类与父类拥有同名成员时,子类成员会屏蔽父类对同名成员的直接访问,这种现象称为隐藏(Hiding),也叫重定义。
注意,只要函数名相同就构成隐藏,不需要参数列表一致。这与重载不同,重载必须在同一作用域内。
class Person {
public:
void fun() { cout << "Person::func()" << endl; }
protected:
int _num = 111;
};
class Student : public Person {
public:
void fun(int) { cout << "Student::func(int)" << endl; }
void Print() {
cout << _num << endl; // 输出 999,子类成员
cout << Person::_num << endl; // 显式访问父类成员,输出 111
}
protected:
int _num = 999;
};
int main() {
Student s;
s.fun(); // 报错:找不到无参版本
s.Person::fun(); // 显式调用父类版本
return 0;
}
实际开发中,尽量避免在继承体系里定义同名成员,以减少混淆。
四、默认成员函数的生命周期
设计理念上,父类负责初始化父类部分,子类负责初始化子类部分。
1. 构造函数
子类构造函数必须调用父类构造函数来初始化继承来的成员。如果在初始化列表中未显式指定,编译器会自动尝试调用父类的默认构造函数。若父类没有默认构造函数,则必须手动指定。
class Person {
public:
Person(const char* name) : _name(name) {}
protected:
string _name;
};
class Student : public Person {
public:
// 必须调用父类构造函数,且不能直接初始化基类成员
Student(const char* name, int id) : Person(name), _id(id) {}
protected:
int _id;
};
2. 拷贝构造与赋值重载
子类拷贝构造和赋值运算符同样需要处理父类部分的复制。对于赋值重载,由于存在隐藏,通常需要显式调用父类的赋值运算符。
class Student {
public:
Student& operator=(const Student& s) {
if (this != &s) {
Person::operator=(s); // 显式调用父类逻辑
_id = s._id;
}
return *this;
}
};
3. 析构函数
析构函数的调用顺序至关重要。先析构子类,再析构父类。这是因为子类可能依赖父类资源,必须先释放子类部分。
C++ 标准规定,子类析构完成后会自动调用父类析构,无需手动调用。如果手动调用 Parent::~Parent(),可能会破坏自动调用的机制,导致重复析构或顺序错误。
五、友元与静态成员
- 友元不继承:父类的友元函数无法访问子类新增的私有或保护成员。友元关系是单向且非继承的。
- 静态成员共享:静态成员属于类而非对象。在继承体系中,无论派生多少子类,静态成员只有一个实例,所有类共享同一份数据。
六、多继承与菱形问题
实践中应谨慎使用多继承。单继承结构简单,而多继承容易引发菱形继承问题。
菱形继承的弊端
当一个类同时继承两个都继承了同一个基类的类时,会产生两份基类副本。这导致:
- 数据冗余:浪费空间。
- 二义性:访问基类成员时编译器不知道选择哪条路径。
class Person { public: int _age; };
class Student : public Person { int _num; };
class Teacher : public Person { int _id; };
class Assistant : public Student, public Teacher { };
// Assistant as; as._age = 19; // 报错:访问不明确
虚拟继承解决方案
通过引入 virtual 关键字,可以让多个派生类共享同一个基类实例,从而解决冗余和二义性。
class Student : virtual public Person { };
class Teacher : virtual public Person { };
虚拟继承的原理是在运行时通过偏移量计算来定位基类成员,确保最终对象中只有一份基类数据。
七、继承与组合
最后谈谈设计模式的选择。
- 继承(Is-A):白箱复用,耦合度高。父类细节对子类可见,父类变更可能影响子类。适用于明确'是一个'的关系(如教师是人)。
- 组合(Has-A):黑箱复用,耦合度低。对象内部细节不可见,仅通过公共接口交互。适用于'有一个'的关系(如车有轮胎)。
最佳实践:优先使用组合。除非必须实现多态,否则组合通常比继承更灵活、更安全。

