本篇核心知识:继承与派生、继承方式、成员权限变化、构造 / 析构顺序、同名成员处理、多继承、菱形继承、虚继承、对象赋值兼容规则
一、继承与派生(概念)
概念
继承是从已有类(父类 / 基类) 快速创建**新类(子类 / 派生类)**的机制,子类会获得基类成员,并可扩展新成员。
特性
目的:代码复用、快速扩展、层次化设计。
子类 = 基类成员 + 新增成员。
不能被继承:构造函数、析构函数、赋值运算符重载、友元。
语法
class 子类 : 继承方式 基类1, 继承方式 基类2...
{
新增成员;
};
三种继承分类
-
单继承:一个子类只有一个直接基类。
-
多继承:一个子类有多个直接基类。
-
多级继承:子类再派生子类(爷→父→子)。
二、继承方式与成员权限
概念
继承方式决定基类成员在子类中的最终访问权限。
三种继承方式
-
public(公有继承)
基类 public → 子类 public
基类 protected → 子类 protected
基类 private → 子类不可访问
-
protected(保护继承)
基类 public → 子类 protected
基类 protected → 子类 protected
基类 private → 子类不可访问
-
private(私有继承)
基类 public → 子类 private
基类 protected → 子类 private
基类 private → 子类不可访问
核心结论
基类private成员无论如何继承,子类都无法直接访问。
公有继承最常用,不改变基类权限结构。
权限只限制外部访问 ,基类私有成员依然会被继承下来(占内存)。
三、继承后的内存布局
概念
子类对象内存 = 基类部分内存 + 子类新增成员内存。
特性
基类所有数据成员都会被复制到子类中(包括 private)。
内存大小 = sizeof (基类) + sizeof (子类新增)。
内存对齐规则不变。
示例
class Base { int a; };
class Son : public Base { int b; };
// sizeof(Son) = 4 + 4 = 8
四、子类构造与析构顺序(重点)
概念
创建子类对象时,必须先构造基类部分;销毁时先析构自身,再析构基类。
构造顺序
-
虚基类优先(按声明顺序)
-
非虚基类(按继承声明顺序,从左到右)
-
对象成员(按类内声明顺序)
-
子类自身构造
析构顺序
与构造完全相反。
代码示例
class A { public: A(){cout<<"A构造";} ~A(){cout<<"~A";} };
class B { public: B(){cout<<"B构造";} ~B(){cout<<"~B";} };
class C : public A, public B { public: C(){cout<<"C构造";} ~C(){cout<<"~C";} };
int main(){
C c;
// 输出:A构造 B构造 C构造 ~C ~B ~A
}
子类初始化基类成员
必须在初始化列表调用基类构造:
// 基类
class Base {
public:
int a;
// 基类构造函数
Base(int x) : a(x) {}
};
// 子类
class Sub : public Base {
public:
int b;
// 子类构造:基类没有无参构造函数就必须在 初始化列表 调用基类构造
// 子对象的初始化也必须在初始化列表中进行
Sub(int x, int y) : Base(x), b(y) {
// 不能初始化基类成员,基类成员的初始化、给值只能在基类的构造函数中
}
};
int main()
{
Sub obj(100,10);
// obj.a = 100; obj.b = 10;
}
拓展
一、必须在初始化列表调用基类构造的情况
只要 基类没有无参构造函数(默认构造) 就 必须在初始化列表手动调用基类构造!
必须写的 3 种场景
-
基类只有带参构造,没有无参构造
-
基类写了构造函数,但都是带参的
-
基类构造需要传参数才能初始化
只要满足上面任意一条 → 必须在初始化列表调用基类构造
必须写的代码样例
class Base {
public:
// 只有带参构造 → 子类必须手动调用
Base(int x) {}
};
class Son : public Base {
public:
// 必须写:Base(x)
Son(int x) : Base(x) { }
};
二、不用在初始化列表写基类构造的情况
只要 基类有无参构造函数(默认构造) 就 不用写,编译器会自动调用!
不用写的 2 种场景
-
基类完全没写构造函数(编译器自动生成无参)
-
基类自己写了无参构造
满足任意一条 → 子类不用写初始化列表调用基类
不用写的代码样例
class Base {
public:
// 有无参构造 → 子类不用手动调用
Base() {}
};
class Son : public Base {
public:
// 不用写 Base(),编译器自动加
Son() { }
};
五、赋值兼容规则(对象 / 指针 / 引用)
概念
公有继承下,子类对象可以当作基类对象使用(安全向上转换)。
规则
-
子类对象 → 赋值给 基类对象(切片,只复制基类部分,自己的部分被浪费)
-
子类对象 → 初始化 基类引用(无切片,无拷贝,仅仅是别名,安全)
-
子类地址 → 赋值给 基类指针(存储子类首地址)
反向不允许
基类不能自动转为子类(不安全,会越界)。
示例
Father objFa;
Father *pFa = &objFa;
Self objSelf;
Self *pSe = &objSelf;
pFa = &objSelf; // 合法
pSe = &objFa; // 非法
objFa = objSelf; // 合法
objSelf = objFa; // 非法
六、同名成员处理
概念
子类与基类成员同名时,优先访问子类成员,基类成员被隐藏。
访问基类同名成员
必须加作用域::。
代码示例
class Base {
public:
int num = 10;
};
class Son : public Base {
public:
int num = 20;
void show() {
cout << num; // 子类 20
cout << Base::num; // 基类 10
}
};
多继承同名冲突
多个基类有同名成员 → 必须用基类名::明确指定。
七、多继承
概念
一个子类继承多个基类。
语法
class 子类 : 继承方式 基类1, 继承方式 基类2...
{};
问题
可能出现同名成员歧义。
可能出现菱形继承(重复继承祖父类)。
八、菱形继承与虚继承(虚基类)
概念
多继承中出现:A 派 B、A 派 C,B 和 C 派 D → D 中会有两份 A 成员,造成冗余与歧义。
问题
内存浪费
成员歧义
数据不一致
解决方案:虚继承 virtual
让多个子类共享同一份基类成员。
语法
class B : virtual public A {};
class C : public virtual A {}; // virtual前后两种写法都可以
class D : public B, public C {};
原理
虚继承会给子类加一个虚基类指针(占用一个指针的内存)。
最终派生类中只保留一份虚基类成员。
虚继承构造规则
虚基类构造优先于同一层所有非虚基类。
最终子类必须直接初始化虚基类。
九、重点
-
继承是为了复用 + 扩展。
-
三种继承方式只改变权限,不改变内存。
-
基类私有成员会被继承,但子类不能访问。
-
构造顺序:虚基类 → 基类 → 对象成员 → 自己。
-
析构与构造相反。
-
子类可自动转基类,反之不行。
-
同名成员用
::区分。 -
多继承容易歧义,菱形继承用虚继承解决。