继承和多态

继承机制的出现:

  • 复用已有的目标代码。
  • 对事物进行分类:
    • 派生类是基类的具体化。
    • 把事物(概念)以层次结构表示出来,有利于描述和解决问题。
  • 增量开发的需要。

单继承

声明不能带基类列表:

1
2
3
4
5
6
7
class UndergraduateStudent : public Student; // 错误声明
class UndergraduateStudent; // 正确声明

// 直接完整定义(继承+类体)
class UndergraduateStudent : public Student {
...
}

继承方式(public / protected / private)

语法:

1
2
3
class Derived : 继承方式 Base {
// ...
};

常见三种继承方式:

  • public 公有继承(最常用,表示“是一种”关系)
  • protected 受保护继承
  • private 私有继承(更像“以……实现”的关系)

假设在基类 Base 中有:

1
2
3
4
5
6
7
8
class Base {
public:
int pub;
protected:
int prot;
private:
int priv;
};

派生类中,这三种继承方式对“在派生类里的可见性”影响如下:

继承方式 Base::pubDerived Base::protDerived Base::privDerived
class D : public Base public protected 不可访问
class D : protected Base protected protected 不可访问
class D : private Base private private 不可访问

注意:

  • 基类的private成员在派生类中始终不可访问(只能通过基类的public/protected成员间接访问)。
  • “不可访问”是指:在派生类的成员函数里不能直接写privthis->priv等代码;
    但派生类对象的内存中仍然包含这一成员,只是只能通过基类的接口(如Base::setPriv()这类函数)来间接读写它。

public 继承的典型例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Student {
public:
int id;
protected:
char nickname[16];

public:
void set_ID(int x) { id = x; }
void setNickname(const char *s) { strcpy(nickname, s); }
virtual void showInfo() { cout << nickname << " : " << id << endl; }
};

class UndergraduateStudent : public Student { // 公有继承
int dept_no;

public:
void setDeptNo(int x) { dept_no = x; }
void showInfo() { // 覆盖虚函数
cout << dept_no << " : " << nickname << endl; // 这里可以访问 protected 成员 nickname
}
};
  • id 在派生类中仍为 public,对象外部可以通过 UndergraduateStudent 实例访问。
  • nickname 在派生类中为 protected,只能在 Student / UndergraduateStudent 的成员函数中访问。
  • 重写 virtual showInfo() 为后续的多态打下基础。

初始化和销毁

  • 派生类对象的初始化由基类和派生类共同完成。

  • 构造函数的执行次序为基类的构造函数->派生类对象成员类的构造函数->派生类的构造函数。

  • 析构函数的执行次序与构造函数刚好相反。

  • 基类构造函数的调用:

    • 缺省的话执行基类默认构造函数。
    • 如果要执行基类的非默认构造函数,则必须在派生类构造函数的成员初始化表里指出。
    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
    class A {
    int x;
    public:
    A() { x = 0; }
    A(int i) { x = i; }
    };

    class B : public A {
    int y;
    public:
    B() { y = 0; }
    B(int i) { y = i; }
    B(int i, int j) : A(i) { y = j; }
    };

    B b1; // 执行A::A()和B::B()
    B b2(1); // 执行A::A()和B::B(int)
    B b3(0, 1); // 执行A::A(int)和B::B(int, int)

    // 如果B的构造函数和A一致,可以直接使用using A::A;来继承A的构造函数

    // 拷贝构造函数/移动构造函数
    class C : public A {
    ...
    // 显式调用A的拷贝构造函数/移动构造函数,如果你不写的话编译器会自动生成默认的移动/拷贝构造(满足条件时),里面也会自动去调用基类的那个构造函数
    C(const C& other) : A(other) {} // 直接传参即可
    C(C&& other) : A(std::move(other)) {} // 要将左值变为右值
    }

虚函数

  • 类型相容:
    • 类与类之间的相容关系
    • “类型相容->赋值相容”:如果类型相容,就可以做赋值/初始化。
  • 对象赋值:A a; B b; class B : public A
    • a = b合法吗,但是赋值后a只保留A的部分,b作为派生类B独有的那部分属性在a里不复存在。
    • 用“对象的身份发生变化 / 属于派生类的属性已不存在”来形容这个切片。
  • 指针/引用赋值(不发生切片)
    • B* pb; A* pa = pb; class B : public A
    • B b; A& a = b; class B : public A
    • 基类指针/引用可以指向派生类对象(类型相容);对象本身没变,仍然是一个B,只是通过A*/A&的视角去看它。
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
class A {
int x, y;
public:
void f();
};

class B : public A {
int z;
public:
void f();
void g();
};

// 把派生类对象赋值给基类对象
A a; B b;
a = b; // OK
b = a; // Error
a.f(); // A::f()

// 基类的引用或指针可以引用或指向派生类对象
A& r_a = b; // OK
A* p_a = &b; // OK
B& r_b = a; // Error
B* p_b = &a; // Error

func1(A& a) { a.f(); }
func2(A* pa) { pa->f(); }

func1(b); // 静态类型为A&,仍然调用A::f()
func2(&b); // 静态类型为A*,仍然调用A::f()

虚函数绑定

前期绑定(Early Binding):

  • 发生在编译时。
  • 根据表达式的静态类型决定调用哪个函数。
  • 效率高,但是多态性差。
  • virtual成员函数默认使用前期绑定。

动态绑定(Late Binding):

  • 发生在运行时。
  • 根据对象的实际类型(动态类型)决定调用哪个函数。
  • 灵活性高(支持多态),效率略低(要查虚函数表)。
  • 必须在基类函数前显示写virtual
1
2
3
4
5
class A {
...
public:
virtual void f();
};

限制:

  • 如基类中被定义为虚成员函数,则派生类中对其定义的成员函数均为虚函数(都在虚函数表里)。
  • 只有类的非静态成员函数可以是虚函数。
  • 静态成员函数不能是虚函数(static成员只有一份存储)。
  • 构造函数不能是虚函数(构造函数会负责给虚函数创建虚函数表来进行绑定,如果构造函数都为虚函数则无法创建),析构函数可以(往往应当)是虚函数。
动态绑定的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
int x, y;
public:
virtual void f();
virtual void g();
void h();
};

class B : public A {
int z;
public:
void f() override;
void h();
};

A a; B b;
A* p;
  • 每个含有虚函数的类,编译器通常会为其生成一张虚函数表(vtable):
    • A_vtable 里放着指向 A::f, A::g 的函数指针;
    • B_vtable 里放着指向 B::f, A::g 的指针(f被覆盖,g继承)。
  • 每个对象里“额外藏着一个指针”(通常称为vptr),指向它所属类的vtable
    • p = &a;,则p里的vptr指向A_vtable
    • p = &b;,则vptr指向B_vtable
  • 调用p->f()时,流程就是:
    通过vptr找到vtable,然后在vtable的对应槽里取出函数指针再调用,从而在运行时选中A::fB::f

虚函数表查询

构造函数中的虚函数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
public:
A() { f(); } // 在构造函数里调用虚函数
virtual void f();
void g();
void h() { f(); } // 普通成员中调用虚函数
};

class B : public A {
public:
void f() override;
void g();
};

B b; // A::A(), A::f, B::B()
A *p = &b;
p->f(); // B::f
p->g(); // A::g
p->h(); // A::h, B::f, A::g
  • A的构造函数中调用f()
    • 即使实际对象是B,也只会调用A::f,不会调到 B::f
    • 因为这时对象还处于“正在构造A部分”的阶段,动态类型还视为A
  • 构造完成以后(例如通过A* p = &b; p->f();),才会按实际类型B做动态绑定,调用B::f

final和override

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct B {
virtual void f1(int) const;
virtual void f2();
void f3();
virtual void f5(int) final;
};

struct D : B {
void f1(int) const override; // 正确:签名与B::f1完全一致
void f2(int) override; // 错误:B中没有形如f2(int)的虚函数
void f3() override; // 错误:f3不是虚函数,不能override
void f4() override; // 错误:基类里根本没有f4
void f5(int); // 错误:B中f5已被标记为final,禁止再重写
};
  • override:告诉编译器“这个函数应该重写某个基类虚函数”。
    • 若签名对不上 / 基类没有对应虚函数,编译期报错,防止“写错函数原型导致没重写成功”的隐患。
  • final
    • 作用在虚函数上:禁止派生类继续重写该虚函数。
    • 作用在类上:该类不能再被继承。

访问控制

访问控制检查在编译期按静态类型来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct B {
protected:
virtual void f() {} // 受保护的虚函数
};

struct D : B {
public:
void f() override {} // 在派生类中把 f 放宽为 public
};

int main() {
D d;
d.f(); // OK:检查D::f,是 public

B* pb = &d;
pb->f(); // 编译期按B::f检查,B::f是protected,类外不可
}
友元和protected

友元关系不继承、不传递,也不靠“同名”共享。谁把你声明成friend,你就只能操作谁的私有/保护成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base {
protected:
int prot_mem; // protected 成员
};

class Sneaky : public Base {
friend void clobber(Sneaky&); // 友元1
friend void clobber(Base&); // 友元2
int j; // 默认private
};

void clobber(Sneaky &s) {
s.j = 0; // OK,访问Sneaky::j(private)
s.prot_mem = 0; // OK,访问Sneaky::prot_mem(继承来的protected)
}

void clobber(Base &b) {
b.prot_mem = 0; // 错误,不能访问Base的protected
}

纯虚函数和抽象类

纯虚函数(pure virtual):

1
2
3
4
class AbstractClass {
public:
virtual void f() = 0; // 纯虚函数
};
  • 在函数原型后面写=0,表示“这个函数在这个类里没有实现(或视作没有)”。
  • 通常只给声明,不写函数体。

抽象类:

  • 至少包含一个纯虚函数的类就是抽象类。
  • 抽象类不能直接创建对象:
    1
    2
    AbstractClass a;    // 错
    AbstractClass* p; // 只能通过指针/引用使用
  • 抽象类给派生类提供一个接口/框架,派生类必须实现这些纯虚函数。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Figure {
public:
virtual void display() = 0; // 抽象接口
};

class Rectangle : public Figure {
public:
void display() override { /* 画矩形 */ }
};

class Ellipse : public Figure {
public:
void display() override { /* 画椭圆 */ }
};

纯虚函数和抽象类的应用非常广泛,常见的包括抽象工厂模式(Abstract Factory),和java的其实差不多。

虚析构函数

只要通过“基类指针删除派生类对象”,基类析构函数就应该是虚函数。

  • 通过基类指针管理派生类对象:

    1
    2
    3
    4
    5
    class B { ... };
    class D : public B { ... };

    B* p = new D;
    delete p; // 这里安全吗?

    如果~B()不是virtual

    • delete p;只会调用B::~B()
    • D自己那部分资源(比如动态分配的内存)不会被释放;
  • 有资源的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class mystring { ... };
    class B { ... };
    class D : public B {
    mystring* name = new mystring; // 在堆上申请资源
    public:
    ~D() { delete name; }
    };

    B* p = new D;
    delete p; // ?调到哪一个析构?

    如果B::~B()不是虚函数:delete p;只执行B::~B()D::~D()完全不会被调用,name永远没被delete,资源泄漏。

所以需要虚析构函数:

1
2
3
4
class B {
public:
virtual ~B() = default; // 或自己写清理逻辑
};

那么delete p;会根据对象实际类型D进行动态绑定:

  • 先调用D::~D()(释放nameD的资源);
  • 再调用B::~B()(释放基类B的资源)。

私有继承

private继承表示“implemented‑in‑term‑of”(用来实现派生类),不是表示“is‑a”(public表示这样的关系,所以使用共有继承的时候要考虑清楚)。派生类可以重用基类的实现(包括protected成员、虚函数等),但并不把基类的接口暴露给类外用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Base {
public:
void publicAPI() { std::cout << "Base::publicAPI\n"; }
protected:
void protectedImpl() { std::cout << "Base::protectedImpl\n"; }
virtual void hook() { std::cout << "Base::hook\n"; }
};

class Impl : private Base { // private继承:表示“用 Base 来实现 Impl”
public:
void doWork() {
protectedImpl(); // OK:protected在派生类中可见
hook(); // 可以重用/调用虚函数(按动态绑定规则)
}
// 可以重写虚函数并在派生类内部调用或调整行为
void hook() override { std::cout << "Impl::hook\n"; }
};

Impl impl;
impl.doWork(); // OK:使用 Impl 的公开接口
// impl.publicAPI(); // 错:Base::publicAPI 在 Impl 中成为 private,类外不可见
// Base* pb = &impl; // 错:从 Impl* 到 Base* 的隐式转换对外部是不可访问的

默认参数值

虚函数是动态绑定的,但默认参数值是静态绑定的,所以“绝对不要在派生类里重新定义继承来的默认参数”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class A {
public:
virtual void f(int x = 0) = 0;
};

class B : public A {
public:
virtual void f(int x = 1) { std::cout << x; } // 改了默认参数
};

class C : public A {
public:
virtual void f(int x) { std::cout << x; } // 只改实现,不改默认参数
};

A* p_a;
B b;
p_a = &b;
p_a->f(); // 打印0,参数看的是静态绑定的类型A

A* p_a1;
C c;
p_a1 = &c;
p_a1->f(); // 打印0,参数看的是静态绑定的类型A

多继承

定义:

1
2
3
class Derived : 继承方式1 Base1, 继承方式2 Base2, ... {
// ...
};
  • 继承方式和访问控制都和单继承一致,派生类拥有所有基类的所有成员。
  • 派生类包含每个基类的虚函数表的指针。

基类顺序和名冲突

基类顺序
1
2
3
4
class A { int x; ... };
class B : A { ... };
class C : A { ... };
class D : B, C { ... }; // 多继承

class D : B, CB, C的顺序会决定:

  • 构造/析构时调用基类构造/析构函数的次序(从左到右的顺序,和构造函数中的顺序无关,析构相反)。
  • 基类子对象在D对象内的存储布局顺序。
名冲突

当多个基类里有同名成员时,需要写成:

1
2
d.B::x; // 明确用B里的x
d.C::x; // 明确用C里的x
虚基类

如果BC都从A公有继承,而D又继承BC,那么D里会有两份A子对象(两份A::x)。
为了解决这个问题引入了虚基类:

1
2
3
4
class A { ... };
class B : virtual public A { ... };
class C : public virtual A { ... }; // 两种写法等价:virtual + public
class D : B, C { ... };
  • 原来D有两份A(B::AC::A),改成虚继承后,D只保留一份共享的A子对象,BC都引用这同一份。
  • 所以D不再有B::xC::x两份A::x,而是一份合并后的A::x
  • 虚基类的构造函数由“最派生类”负责调用,上例就是D的构造函数负责构造那唯一的一份A
  • 在构造顺序上,虚基类先于非虚基类构造。