继承与派生

继承与派生

目录

基类和派生类

基本概念

  • 定义新类B时B拥有某个已有的类A的全部特点(成员)

    称A-基类(父类),B-派生类(子类)

    • 派生类对基类进行修改扩充得到的
    • 扩充:添加新成员
    • 修改:重新编写继承来的成员
  • 派生类一经定义,可以独立使用,不依赖于基类

  • 内存空间:基类对象大小+派生类对象自己的成员变量大小(基类对象存储位置在新增之前)

    image-20260321150856082

  • 在派生类中访问基类同名成员(叫“隐藏”或“覆盖”)利用基类::变量名访问,缺省情况下访问的是派生类的成员

范式

1
2
3
class 派生类名 : 派生说明方式符 基类名{
...
}

派生类的各个成员函数不得访问基类中的private成员

派生说明方式符:public/private/protected

场景

学生(基类)→中学生/大学生/小鞋生/研究生

  • 共同属性:姓名、学号、性别、成绩

  • 特有:大学专业,研究生导师,中学生竞赛

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class CStudent{
private:
string sName;
int nAge;
public:
bool IsThreeGood(){}
void SetName(const string &name){
sName=name;
}
// ...
};

class CUndergraduateStudent : public CStudent{
private:
int nDepartment;
public:
bool IsThreeGood(){...} //派生类同名函数将隐藏基类函数(不建议)
bool Membergroup(){...}
};

class CGraduateStudent : public CStudent{
private:
int nDepartment;
char SupervisorName[20];
public:
int CountSalary(){...}
}

继承与复合的区别:

复合:有(B里有A)

继承:“是”且更多(B是A(及其延伸))

复合关系避免循环定义

⚠️ 避免循环定义,设置对象指针数组

image-20260321151339649

派生类的成员组成和访问权限

访问权限说明符:protected

protected可访问范围介于private成员和public成员之间

⚠️ 派生类的成员和友元函数可以访问当前对象的来自基类保护成员

(此处有个话术:应该是可以访问自己/自己的亲兄弟(同类对象)的爸爸传下来的保护成员,条件是同一个爸爸的下的同一个类,结果在传下来的同一个类的兄弟或者我的保护成员)

自己的这个类的子孙的对象也可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base {
protected:
int x;
};

class D1 : public Base {
public:
void f(D1& other) { other.x = 1; } // ✅ OK:同类对象
void f2(D1* child) { child->x = 1; } // ✅ OK:D1 的派生类对象
void g(Base& b) { /* b.x = 1; */ } // ❌ ERROR:基类对象
};

class D2 : public Base {
// D2 也是 Base 的儿子
};

class D1 {
public:
void h(D2& d2) { /* d2.x = 1; */ } // ❌ ERROR!
// D2 和 D1 都是 Base 的派生类,但彼此无关
};
  • this指针指向的对象
  • 也可以访问其他同类对象的来自基类保护成员
  • main函数可以理解为全局函数,不可以访问保护成员

具体可以参见下面这个例子

image-20260323193114346

尤其是这个👇

image-20260323193154033

三种继承方式

⚠️ 继承来的私有成员仍都是私有的,且都是派生类直接不可访问的!只能通过基类的public接口或者protected接口访问!虽然真的很怪

  • 公有继承:保持原样

  • 私有继承:全部私有(“孙”类访问不了)

  • 保护继承:公有变保护,其余不变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base {
private:
int secret = 42;
protected:
int getSecret() { return secret; } // 提供给子类的合法通道
};

class Derived : public Base {
public:
void test() {
// secret = 10; // 错误!编译器报错:不可访问
int val = getSecret(); // 正确!通过受保护接口访问
}
};

image-20260323194827354

一级警报:⚠️⚠️派生类自身的成员函数不能访问自己继承来的私有成员变量是派生类能访问自己私有继承来的公有和保护成员(哭笑不得)

想访问自己继承来的私有成员变量,要间接,即通过例如:继承来的基类的public接口或者protected接口访问!

派生类的构造和析构

派生类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Bug{
private:
int nLegs;
int nColor;
public:
int nType;
Bug(int legs,int color);
void PrintBug(){}
};
class FlyBug:public Bug{
private:
int nWings;
public:
FlyBug(int legs,int color,int wings);
};

Bug::Bug(int legs,int color){
nLegs=legs;
nColor=color;
}
// 错误!不能自己碰基类继承来的私有成员
// FlyBug::FlyBug(int legs,int color,int wings){
// nLegs=legs; //error!
// nColor=color; //error!
// nType=1; //ok
// nWings=wings;
//}

//正确如下,调用基类的构造函数
FlyBug::FlyBug(int legs,int color,int wings):Bug(legs,color){
nType=1; //⚠️一定不能放到初始化列表,只能通过赋值或者基类初始化
nWings=wings;// 可以放到初始化列表
}

int main(){
FlyBug fb(2,3,4);
fb.PrintBug();
fb.nType=1;
fb.nLegs=2; //error!
return 0;
}

因此;

  • 创建派生类对象,需要调用基类的构造函数
    • 初始化从基类继承来的成员
    • 总是先执行基类的构造函数(析构时则相反,与封闭类类似,脱白大褂再迎来生命的终结
  • 调用方式:
    • 显示构造(如上例)
    • 隐式:缺省情况下默认调用基类的默认构造函数

包含成员对象的派生类的构造函数

image-20260323200921209

顺序分别同,自推不难

⚠️考点:public继承的赋值兼容原则

1
2
3
4
class base{};
class derived : public base{};
base b;
derived d;
  • ⚠️ 派生类的对象可以赋值给基类对象

    b=d

  • ⚠️ 派生类对象可以初始化基类引用

base & br=d;

  • ⚠️ 派生类对象的地址可以赋值给基类指针(更详细参见后文)

base *pb=& d;

⚠️private/protected继承上述操作

基类指针不能访问基类的私有成员,无论它指向的是基类对象还是派生类对象。

访问控制是静态的、基于类定义的,与指针类型无关:

访问场景能否访问基类 private
外部代码通过 Base*
外部代码通过 Derived*
派生类成员函数中通过 Base*
派生类成员函数中通过 thisDerived*
基类自己的成员函数中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base {
int private_val; // private
protected:
int protected_val;
public:
int public_val;
};

class Derived : public Base {};

int main() {
Derived d;
Base* p = &d;

// p->private_val = 1; // ❌ 编译错误!
// p->protected_val = 1; // ❌ 外部也不能访问 protected
p->public_val = 1; // ✅ 只能访问 public
}

即使在派生类内部:

1
2
3
4
5
6
7
8
9
class Derived : public Base {
public:
void func() {
Base* p = this;
// p->private_val = 1; // ❌ 仍然错误!
p->protected_val = 1; // ✅ protected 在派生类内部可以
p->public_val = 1; // ✅ public 随时可以
}
};

派生类与基类的指针类型转换

基类与派生类强制类型转换

⚠️ 公有派生下:

派生类对象的地址可以赋值给基类指针

base *pb=& d;

  • *pb指向一个Derived对象,可以看作一个Base类的对象,访问它的public成员(对外部来说)

  • 即便如此,也不能通过基类指针访问基类没有,而派生类有的成员

    ☀️此时,通过 ptrBase 去观察它指向的内存,编译器只会根据 Base 类的模板去匹配内存字段

可以强制类型转换

1
2
Base * ptrBase= & objDerived;
Derived *ptrDerived = (Derived*)ptrBase

(但有点危险)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
using namespace std;

class Base {
protected:
int n;
public:
Base(int i) : n(i) {
cout << "Base " << n << " constructed" << endl;
}
~Base() {
cout << "Base " << n << " destructed" << endl;
}
void Print() { cout << "Base:n=" << n << endl; }
};

class Derived : public Base {
public:
int v;
Derived(int i) : Base(i), v(2 * i) {
cout << "Derived constructed" << endl;
}
~Derived() {
cout << "Derived destructed" << endl;
}
void Func() { }
void Print() {
cout << "Derived:v=" << v << endl;
cout << "Derived:n=" << n << endl;
}
};

int main() {
Base objBase(5);
Derived objDerived(3);
Base * pBase = &objDerived;

// pBase->Func(); // err; Base类没有Func()成员函数
// pBase->v = 5; // err; Base类没有v成员变量
pBase->Print(); // 调用基类函数Print()

// Derived * pDerived = &objBase; // error
Derived * pDerived = (Derived *)(&objBase); // 强制转换,慎用
pDerived->Print(); // 慎用,可能出现不可预期的错误
pDerived->v = 128; // 往别人的空间里写入数据 -> 可能引起问题

objDerived.Print();
return 0;
}
  1. ⚠️ pBase->Print();调用的是被隐藏的基类的print()函数,而不是派生类重载的那个
  2. 关于扩张(多出来未知)

image-20260323203703816

直接基类与间接基类

类A派生B,类B派生C,…

  • 在声明派生类时,派生类的首部只要列出它的直接基类
    • 间接基类不要在首部列出
    • 自动会向上继承
    • 派生类的成员包括:
      • 派生类自己定义的成员
      • 直接基类中定义的成员
      • 间接基类的全部成员(默认了为根基类)

关于"自动向上继承"

C 确实会继承 A 的全部成员(包括间接继承),但访问权限可能逐层衰减。如果 B 对 A 是 private 继承,那么 C 中 A 的 publicprotected 成员会变成 B 的 private,C 就无法访问这些从 A 来的成员。不是"全部成员都能用",而是"全部成员都在对象内存里"。

关于构造顺序

虽然声明时只写直接基类 class C : public B,但构造时编译器会自动先构造 A(间接基类),再构造 B(直接基类),最后构造 C。析构顺序严格相反。

多继承(考试不要求,不过有点助于进一步理解)

多继承指一个类可以从多个基类派生,以继承多个基类的成员。

1
2
3
4
// 多继承语法格式
class derived : access-specifier₁ base₁, access-specifier₂ base₂, ... {
// 派生类成员定义
};
  • access-specifier 可取值为 privateprotectedpublic,用于指定每个基类的继承权限。

多继承的派生类构造函数

2.1 构造函数定义示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class base1 {
int i;
public:
base1(int n) { i = n; }
};

class base2 {
int j;
public:
base2(int n) { j = n; }
};

class derived : public base1, public base2 {
public:
derived(int x);
};

// 派生类构造函数:在初始化列表中调用基类构造函数
derived::derived(int x) : base1(x), base2(0) { }

2.2 构造函数调用顺序

  • 派生类对象创建时,先按继承顺序调用基类构造函数,再调用派生类构造函数。
  • 若派生类包含成员对象,成员对象的构造函数在基类构造函数调用结束后依次执行,最后调用派生类构造函数。
  • 完整流程:
    1. 按继承顺序调用基类构造函数
    2. 依次调用成员对象的构造函数
    3. 调用派生类的构造函数

多继承中基类构造函数的重复调用

3.1 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

class Base {
public:
int val;
Base() { cout << "Base Constructor" << endl; }
~Base() { cout << "Base Destructor" << endl; }
};

class Base1 : public Base { };
class Base2 : public Base { };
class Derived : public Base1, public Base2 { };

int main() {
Derived d;
return 0;
}

3.2 运行结果与问题分析

1
2
3
4
Base Constructor
Base Constructor
Base Destructor
Base Destructor
  • 问题:Derived 同时继承 Base1Base2,而 Base1Base2 均继承自 Base,导致 Base 类的构造/析构函数被调用两次,产生数据冗余与二义性。
  • 继承关系图:
    1
    2
    3
    4
    5
        Base
    / \
    Base1 Base2
    \ /
    Derived

多重继承的二义性问题

4.1 成员名冲突的二义性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class base1 {
private:
int b1;
void set(int i) { b1 = i; } // 私有成员函数
public:
int i;
};

class base2 {
private:
int b2;
public:
void set(int i) { b2 = i; } // 公有成员函数
int get() { return b2; }
int i; // 与base1的i同名
};

class derived : public base1, public base2 {
public:
void print() {
printf("%d", get());
// set(5); // ❌ 二义性:编译器无法确定调用哪个set()
base2::set(5); // ✅ 正确:指定调用base2的set()
// base1::set(5); // ❌ 错误:base1的set()是私有成员,无法访问
}
};

4.2 外部访问的二义性

1
2
3
4
5
6
7
8
9
int main() {
derived d;
// d.set(10); // ❌ 二义性:存在多个同名set()
// d.base1::set(10); // ❌ 错误:base1的set()是私有成员
d.base2::set(5); // ✅ 正确:指定基类访问
d.base1::i = 5; // ✅ 正确:指定访问base1的i
d.base2::i = 5; // ✅ 正确:指定访问base2的i
return 0;
}

4.3 核心规则

  • 二义性检查在访问权限检查之前执行,无法通过访问权限消除二义性:
    1
    2
    3
    4
    5
    class A { public: void fun(); };
    class B { private: void fun(); };
    class C : public A, public B { };
    C obj;
    obj.fun(); // ❌ 仍报二义性错误,即使B的fun()是私有
  • 解决方法:使用作用域解析符 :: 明确指定要访问的基类成员。