一、继承的基本概念
本质:代码复用+类关系建模(是多态的基础)
class Person { /*...*/ };
class Student : public Person { /*...*/ }; // public继承
- 派生类继承基类成员(数据+方法),可以通过监视窗口检验成员复用。
二、继承中的访问权限控制
访问权限变化表
基类成员访问限定符/继承方式 | public继承 | protected继承 | private继承 |
public成员 | ->派生类public | ->派生类protected | ->派生类private |
protected成员 | ->派生类protected | ->派生类protected | ->派生类private |
private成员 | 不可见(但存在) | 不可见(但存在) | 不可见(但存在) |
关键规则
- private成员:在派生类中始终不可访问(但存在于对象中)
- protected成员:专为继承设计,派生类可访问,外部不可访问。
- 访问权限计算:Min(成员在基类的权限,继承方式),权限等级:public>protected>private
- 默认继承方式:class默认private继承;struct默认public继承。
- 实际开发:优先使用public继承(占实际使用90%以上),慎用protected/private继承。
三、对象赋值转换规则
允许的操作
Student s;
Person p = s; // 对象切片(调用拷贝构造)
Person& rp = s; // 直接引用基类部分
Person* pp = &s; // 直接指向基类部分
禁止的操作
// Person p;
// Student s = p; // 错误!基类无法赋给派生类
关键注意
- 对象切片:派生类->基类赋值时,丢失派生类特有成员。
- 引用转换原理:派生类对象包含完整的基类子对象,无临时变量生成。
类型系统对比:
int i = 0;
const double& rd = i; // 需要const引用(临时变量具有常性)
四、继承中的作用域
核心规则
- 独立作用域:基类和派生类拥有独立的作用域。
- 隐藏/重定义:派生类成员与基类同名时,隐藏基类成员,包括成员变量和函数(无论参数是否一致)。
class Base { public: void func(int) {} }; class Derived : public Base { public: void func() { Base::func(42); // 必须显式指定作用域 } };
重要细节
- 函数隐藏与重载:派生类会隐藏基类所有同名函数(即使参数不同)
- 访问被隐藏成员:使用作用域解析符
Base::member
- 设计建议:避免定义同名非虚函数
五、派生类默认成员函数
1. 构造函数
-
规则
1.当基类存在默认构造函数时:如果基类有隐式或显式的无参构造函数(即默认构造函数),派生 类的构造函数初始化列表不需要显式调用基类构造函数。编译器会自动隐式调用基类的默认构造 函数。示例:
class Person {
public:
Person() {} // 默认构造函数(可以隐式生成)
};
class Student : public Person {
public:
Student() {} // 隐式调用 Person::Person()
};
2.当基类没有默认构造函数时:如果基类的构造函数需要参数,且没有定义无参构造函数,则派生类必须在初始化列表中显示调用基类的某个构造函数,否则会编译报错。示例:
class Person {
public:
Person(int x) {} // 没有默认构造函数
};
class Student : public Person {
public:
Student() : Person(42) {} // 必须显式调用基类构造函数
};
-
原理:基类成员初始化顺序优先于派生类成员
2. 拷贝构造函数
-
规则:需显式调用基类拷贝构造,完成基类部分的深拷贝
-
代码示例:
Student(const Student& s) : Person(s) // 切片调用基类拷贝构造 , _id(s._id) {}
3. 赋值运算符重载
-
规则:需要显式调用基类赋值运算符,处理自赋值情况
-
代码示例:
Student& operator=(const Student& s) { if (this != &s) { Person::operator=(s); // 显式调用基类赋值 _id = s._id; } return *this; }
4. 析构函数
-
规则:
-
析构顺序:派生类->基类(自动调用基类析构)
-
禁止显式调用基类析构函数
-
-
代码示例:
~Student() { // 自动调用Person::~Person() delete _ptr; // 先清理派生类资源 }
六、继承关系与友元
-
规则:基类友元不能访问派生类私有成员,需要额外声明
-
代码示例:
class Student; // 前向声明 class Person { friend void Display(const Person&, const Student&); }; class Student : public Person { friend void Display(const Person&, const Student&); }; void Display(const Person& p, const Student& s) { cout << p._name << endl; // 访问基类保护成员 cout << s._stuNum << endl; // 访问派生类保护成员 }
七、静态成员与继承
-
特性:
-
基类静态成员被所有派生类共享
-
继承的是访问权而非副本
-
静态成员不被包含在对象中,它放在静态存储器。
-
-
代码示例:
class Person { public : Person () {++ _count ;} protected : string _name ; // 姓名 public : static int _count; // 统计人的个数。 }; int Person :: _count = 0; class Student : public Person { protected : int _stuNum ; // 学号 }; class Graduate : public Student { protected : string _seminarCourse ; // 研究科目 }; void TestPerson() { Student s1 ; Student s2 ; Student s3 ; Graduate s4 ; cout <<" 人数 :"<< Person ::_count << endl; //输出:人数 :4 Student ::_count = 0; cout <<" 人数 :"<< Person ::_count << endl; //输出:人数 :0 }
八、复杂继承模型
- 单继承:一个子类只有一个直接父类时称这个继承关系为单继承
- 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:多继承的一种特殊情况
问题
数据冗余和二义性(从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在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 ; // 主修课程
};
void Test ()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a ;
a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
解决方案
虚拟继承(在Assistant的对象中Person成员只有一份。)
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 ; // 主修课程
};
void Test ()
{
Assistant a ;
a._name = "peter";
}
虚拟继承解决数据冗余和二义性的原理
讲解
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。
class A { int _a; };
class B : public A { int _b; };
class C : public A { int _c; };
class D : public B, public C { int _d; };
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
![](https://i-blog.csdnimg.cn/direct/ac3cb9eca702438b86760fc1f3d1a98b.png)
![](https://i-blog.csdnimg.cn/direct/f48a9077de63463fb896bef6a9262b48.png)
- 疑问1:为什么D中B和C部分要去找属于自己的A?
那么大家看看当下面的赋值发生时,d是不是要去找出B/C成员中的A才能赋值过去?
- 疑问2: 为什么要在B和C中存指针,而不直接存距离A的偏移量呢?
①存储内容多样性:
- 多偏移量:菱形继承中,虚基表除存当 前类到虚基类偏移量,在多重继承、复 杂模板实例化等场景,还需存不同条件 下访问虚基类的其他偏移量,用于正确 定位成员。
- 辅助信息:虚基表存储虚基类构造、析 构函数指针等辅助信息,确保在对象构 构造、析构及函数调用时正确操作。仅 用偏移量无法存储这些信息,易致错误。
②支持复杂偏移关系:
- 适应结构变化:使用虚基表指针,面对 复杂继承结构变化(如 B、C 继承路径 新增虚继承层次)时,虚基表可添加新 偏移量或信息,指针仍能正确指向,保 证虚基类成员访问。直接用偏移量,结 构变化时需多处修改,维护扩展困难。
![](https://i-blog.csdnimg.cn/direct/1ffc754f8fda4387bbd002a3b2ce4144.png)
九、继承与组合的选择
特征 | 继承 | 组合 |
---|---|---|
关系性质 | "is-a"关系 | "has-a"关系 |
可见性 | 白箱复用(了解实现细节) | 黑箱复用(接口隔离) |
耦合度 | 高耦合 | 低耦合 |
多态支持 | 支持 | 不支持 |
代码复用方式 | 垂直复用(扩展功能) | 水平复用(功能组合) |
使用建议:
-
优先使用对象组合
-
需要多态特性时使用继承
-
避免过度使用多继承