EagleBear2002 的博客

这里必须根绝一切犹豫,这里任何怯懦都无济于事

C++ 高级程序设计-22-继承

继承

继承机制

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

有利于增量开发

单继承

单继承指派生类只有一个基类。

实例

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 Student {
int id;

public:
char nickname[16];
void set_ID(int x) { id = x; }
void SetNickName(char* s) { strcpy(nickname, s); }
void showInfo() { cout << nickname << " : " << id << endl; }
};

class Undergraduated_Student: public Student { // 继承方式 public,相当于 Undergraduated_Student 里面有一个 Student
int dept_no;
public:
void setDeptNo(int x) { dept_no = x; }
void setID(int x) { ... } // 此处无法访问基类的 id
void showInfo(int x) { cout << dept_no << " : " << nickname << endl;}
private:
// using Student::showInfo;
Student::nickname; // 改变基类成员的访问控制
void setNickName();
}

int main() {
Undergraduated_Student us;
us.showInfo(); // 报错,找不到 showInfo 函数,函数隐藏
// 首先寻找 Undergraduated_Student::showInfo(...),找到之后就不会在其他命名空间找,只有找不到同名函数时才会去基类命名空间中找,解决方式是在 Undergraduated_Student 中添加 using Student::showInfo。
return 0;
}

构造函数、拷贝构造函数、析构函数不能被继承。这些函数是管理对象的资源的,而派生类往往比基类有更多的资源。

继承方式不影响派生类对基类的访问,影响派生类用户对基类的访问。

访问控制

public、private:访问权限只和基类中的访问权限有关

public

  1. public:class Undergraduated_Student: public Student
  2. 原来的 public 是 public,原来的 private 是 private
  3. 如果没有特殊需要建议使用 public

private

  1. private:原来所有的都是 private,但是这个 private 是对于 Undergraduate_Student 大对象而言,所以他自己还是可以访问的。
  2. 默认的继承方式

protected

  1. 如果没有继承的话,protected 和 private 是相同的
  2. 派生类可以访问基类中 protected 的属性的成员。
  3. 派生类不可以访问基类中的对象的 protected 的属性。
  4. 派生类含有基类的所有成员变量
public protected private
public 继承 public protected 不可见
private 继承 private private 不可见
protected 继承 protected protected 不可见

继承的初始化

  1. 派生类对象的初始化
    • 由基类和派生类共同完成
  2. 构造函数的执行次序
    1. 基类的构造函数
    2. 派生类对象成员类的构造函数(注意!)
    3. 派生类的构造函数
  3. 析构函数的执行次序(与构造函数执行顺序相反)
    1. 派生类的析构函数
    2. 派生类对象成员类的析构函数
    3. 基类的析构函数
  4. 基类构造函数的调用
    • 缺省执行基类默认构造函数
    • 如果要执行基类的非默认构造函数,则必须在派生类构造函数的成员初始化表中指出
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
//测试执行和析构顺序
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(const B& b) { //拷贝构造
//首先调用 A 的默认初始化构造函数
//如果想要调用对应拷贝构造函数,必须用成员初始化表声明
//拷贝构造函数
}
};

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)

class B : public A {
public:
//继承下来多版本的构造函数
using A::A; //继承 A 的构造函数
};

虚函数

类型相容

  1. 类型相容
  2. 类型相容是指完全相同的(别名)
  3. 一个类型是另一个类型的子类型(int -> long int)
  4. 赋值相容(不会丢失信息):对于类型相同的变量才有
  5. 如果类型相同可以直接赋值
  6. 子类型可以赋值给父类型
  7. 问题:a 和 b 都是类,a、b 什么类型时,a = b 合法(赋值相容)?B 是 A 的子类型的时候

A a; B b; class B: public A;

对象的身份发生变化(a 和 b 都代表栈上对应大小的内存),B 类型对象变为了 A 类型的对象,属于派生类的属性已不存在,发生对象切片行为(将派生类对象赋值给基类对象)

  • A a = b 调用拷贝构造函数
  • const A &a 函数必然包含的拷贝构造函数中的参数
  • B* pb; A* pa = pb; class B: public A; 因为是赋值相容的,所以可以指针赋值,这种情况类似 Java
  • B b; A &a=b; class B: public A;:对象身份没有发生变化(还是 B)
  1. 把派生类对象赋值给基类对象,基类的引用或指针可以引用或指向派生类对象,不严谨的说,可以说让父类指向子类
  2. 传参的时候尽量不要拷贝传参(存在对象切片问题),而是使用引用传参。
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::f
func2(&b);
  1. func1(b):为什么是 A 的呢?
    1. 对于 B,A 的版本的对应函数被隐藏
    2. 静态绑定是只看形参类型

绑定时间

  1. C++默认静态绑定

前期绑定(Early Binding)(静态绑定)

  1. 编译时刻确定调用哪一个方法
  2. 依据对象的静态类型
  3. 效率高、灵活性差
  4. 静态绑定根据形参决定

动态绑定(Late Binding)

  1. 晚绑定是指编译器或者解释器在运行前不知道对象的类型,使用晚绑定,无需检查对象的类型,只需要检查对象是否支持特性和方法即可。
  2. c++中晚绑定常常发生在使用 virtual 声明成员函数
  3. 运行时刻确定,依据对象的实际类型(动态)
  4. 灵活性高、效率低
  5. 动态绑定函数也就是虚函数。
  6. 直到构造函数返回之后,对象方可正常使用
  7. C++默认的都是静态绑定,Java 默认的都是动态绑定

后期绑定的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
int x, y;

public:
virtual f();
virtual g();
h(); // h 函数是默认的
};
class B : public A {
int z;

public:
f();
h();
};
A a;
B b;
A* p;
//调用情况见图

  • p->f():需要寻找 a 和 b 中的 f()数地址
  • 如果不能明确虚函数个数,没有办法索引
  • 虚函数表(索引表,vtable):大小可变
    • 首先构造基类的虚函数表
    • 然后对派生类中的函数,如果查找了,则会覆盖对应函数来生成虚函数表
  • 对象内存空间中含有指针指向虚函数表
  • (**((char *)p - 4))(p):f 的函数调用(从虚函数表拿数据),p 是参数 this
  • 空间上和时间上都付出了代价
    • 空间:存储虚函数表指针和虚函数表
    • 时间:需要通过虚函数表查找对应函数地址,多调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
public:
A() { f(); }
virtual void f();
void g();
void h() {
f();
g();
}
};
class B : public A {
public:
void f();
void g();
};
//直到构造函数返回之后,对象方可正常使用
//函数调用顺序,重要考试题,依据虚函数表
B b; // A::A(),A::f, B::B()
//为什么调用 A 的 f 而不是 B 的?因为名空间以及 B 没有构造。
A* p = &b;
p->f(); // B::f
p->g(); // A::g,g 是静态绑定
p->h(); // A::h, B::f, A::g
  • 尽量不要在构造函数中调用虚函数,此时的虚函数就是和构造函数名空间相同
  • h()数是非虚接口
    • 有不同的实现:调用了虚函数和非虚函数
    • 可以替换部分的实现
    • 可以使得非虚函数具有虚函数的特性(让全局函数具有多态:将全局函数做成非虚接口)
1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
public:
virtual void f();
void g();
};
class B : public A {
public:
void f(B* const this) { g(); } // this g() this->g();
void g();
};
B b;
A* p = &b;
p->f(); // B::f,b.B::g
  • g()静态绑定
  • 虚函数中调用非虚函数:所有版本是和虚函数一致
  • 非虚函数调用虚函数:正常
  • 虚函数要严格查表,非虚函数静态确定,对应 p->h()
  • 注意每一个函数在调用的时候都会传入一个 const 的 this 指针

注重效率

  1. 默认前期绑定
  2. 后期绑定需显式指出 virtual

定义

  1. 虚函数是指一个类中你希望重载的成员函数,但你使用一个基类指针或引用指向一个继承类对象的时候,调用一个虚函数时,实际调用的就是继承类的版本。
1
2
3
4
class A {
public:
virtual void f();
};
  1. 定义绑定:根据实际引用和指向的对象类型
  2. 方法重定义
  3. 注意:如基类中被定义为虚成员函数,则派生类中对其重定义的成员函数均为虚函数,也就是派生类中的对应函数可以不写虚函数。
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
50
#include <iostream>
using namespace std;

class Parent {
public:
char data[20];
void Function1();
virtual void Function2(); // 这里声明 Function2 是虚函数
} parent;

void Parent::Function1() {
printf("This is parent,function1\n");
}

void Parent::Function2() {
printf("This is parent,function2\n");
}

class Child : public Parent {
void Function1();
void Function2();
} child;

void Child::Function1() {
printf("This is child,function1\n");
}

void Child::Function2() {
printf("This is child,function2\n");
}

int main(int argc, char* argv[]) {
Parent* p;
// 定义一个基类指针
if (_getch() == 'c') // 如果输入一个小写字母 c
p = &child; // 指向继承类对象
else
p = &parent; // 否则指向基类对象
p->Function1();
// 这里在编译时会直接给出 Parent::Function1()入口地址。
p->Function2();
// 注意这里,执行的是哪一个 Function2?
return 0;
}
//输入 c,输出:
// This is parent,function1
// This is child,function2
//输入非 c,输出:
// This is parent,function1
// This is parent,function2

限制

  • 类的成员函数才可以是虚函数
  • 静态成员函数不能是虚函数
  • 内联成员函数不能是虚函数
  • 构造函数不能是虚函数
  • 析构函数可以(往往)是虚函数

final 和 override

  • override:希望以虚函数的形式写(编译器报错,防止漏写 virtual 问题)
  • final:不可以再次重写
1
2
3
4
5
6
7
8
9
10
11
12
13
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; //正确: f1 与基类中的 f1 匹配
void f2(int) override; //错误: B 没有形如 f2(int) 的函数。int f2()?
void f3() override; //错误: f3 不是虚函数
void f4() override; //错误: B 没有名为 f4 的函数
void f5(int); //错误: B 已经将 f5 声明成 final
}

纯虚函数和抽象类

纯虚函数(Java 中的接口)

  1. 声明时在函数原型后面加上 = 0:virtual int f() = 0;
  2. 往往只给出函数声明,不给出实现:可以给出实现,通过函数外进行定义(但是不好访问,因为查到是 0)
1
2
3
4
int f() = 0;
int f(){
Base::f;//显式调用基类中纯虚函数的定义
}

抽象类

  1. 至少包含一个纯虚函数
  2. 不能用于创建对象:抽象类类似一个接口,提供一个框架
  3. 为派生类提供框架,派生类提供抽象基类的所有成员函数的实现
1
2
3
4
class AbstractClass {
public:
virtual int f() = 0;
};
示例一:Figure

1
2
3
4
5
6
7
Figure* a[100];	 // Figure 基类,不会被创建出来
a[0] = new Rectangle();
a[1] = new Ellipse();
a[2] = new Line();
for (int i = 0; i < num_of_figures; i++) {
a[i]->display();
}
  • 基于接口的复用,而不是基于实现的复用

抽象工厂模式

  • Step1:提供 Windows GUI 类库:WinButton
1
2
3
4
WinButton *pb = new WinButton();
pb->SetStyle();
WinLabel *pl = new WinLabel();
pl->SetText();
  • Step2:增加对 Mac 的支持:MacButton,MacLabel
1
2
3
4
MacButton *pb = new MacButton();
pb->SetStyle();
MacLabel *pl = new MacLabel();
pl->SetText();
  • Step3:增加用户跨平台设计的支持,将 Button 抽象出来
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
Button* pb = new MacButton();
pb->SetStyle();
Label* pl = new MacLabel();
pl->SetText();
//创建工厂来保证创建的正确性
class AbstractFactory {
public:
virtual Button* CreateButton() = 0;
virtual Label* CreateLabel() = 0;
};
class MacFactory : public AbstractFactory {
public:
MacButton* CreateButton() { return new MacButton; }
MacLabel* CreateLabel() { return new MacLabel; }
};
class WinFactory : public AbstractFactory {
public:
WinButton* CreateButton() { return new WinButton; }
WinLabel* CreateLabel() { return new WinLabel; }
};
class Button; // Abstract Class
class MacButton : public Button {};
class WinButton : public Button {};
class Label; // Abstract Class
class MacLabel : public Label {};
class WinLabel : public Label {};

AbstractFactory* fac;
switch (style) {
case MAC:
fac = new MacFactory;
break;
case WIN:
fac = new WinFactory;
break;
}

Button* button = fac->CreateButton();
Label* Label = fac->CreateLabel();
  • 抽象工厂模式的类图

虚析构函数

1
2
3
4
5
6
7
8
9
10
11
12
class B {};
class D : public B {};
B* p = new D;
delete p; //如果 B 和 D 的属性是相同的,则没有什么问题

class mystring {};
class B {};
class D : public B {
mystring name;
};
B* p = new D;
delete p; //有问题,因为 D 中比 B 多一个 mystring;如果析构函数不是虚函数,则没有办法正确调用 D 的析构函数;调用派生类析构函数 -> 调用成员对象的析构函数 -> 基类的析构函数

如果有继承的话,最好使用虚析构函数,在调用析构的函数,会先调用基类的析构函数,所以在析构函数中,只需要析构派生类自己的资源就可以了。

缺省参数

缺省值采用静态绑定方式,效率更高。

绝对不要重新定义继承而来的缺省参数值,以免引起阅读歧义:

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
class A {
public:
virtual void f(int x = 0) = 0;
};

class B : public A {
public:
virtual void f(int x = 1) { cout << x; }
};

class C : public A {
public:
virtual void f(int x) { cout << x; }
};

A* p_a;
B b;
p_a = &b;
p_a->f(); // 输出 0

A* p_a1;
C c;
p_a1 = &c;
p_a1->f(); // C::f(),没有缺省参数,但该行能通过静态编译,输出 0

  • 对象中只记录了虚函数入口的位置
  • 默认参数在编译的时候静态绑定(最开始就将 x 绑定到所有的此类函数中去。)
  • 并没有为每一个版本存储记录一个缺省值::因为如果有的话,需要用额外的空间来存储缺省值。
  • 如果给出参数,则进行压栈(1),而往往有时候没有,这时候将默认参数存储虚函数表中。这会影响语言的效率。(避免寻址)
1
2
3
(**((char *)p_a1 - 4))(p_a1)
char *q = *((char *)p_a1 - 4);
(*q)(p_a1, *q+4);

函数的分类

1
2
3
4
5
6
class Shape {
public:
virtual void draw() const = 0;
virtual void error(const string& msg);
int objectID() const;
};

纯虚函数

只有函数接口会被继承

  • 子类必须继承函数接口
  • 必须提供实现代码

一般虚函数

函数的接口及缺省实现代码都会被继承

  • 子类必须继承函数接口
  • 可以继承缺省实现代码
  • 不希望代码被经常修改,微小修改

非虚函数

函数的接口和其实现代码都会被继承

  • 必须同时继承接口和实现代码

继承类型

public inheritance 公有继承

  1. 确定 public inheritance,是真正意义的"IS_A"关系:派生类就是基类,超类中所拥有的性质在子类中仍然成立
  2. Require No more,Promiss No Less(LSP)里氏替换原则:里氏替换原则没有给出实现
  3. 不要定义与继承而来的非虚成员函数同名的成员函数
Penguin 问题

软件外包的时候不知道是谁写的,无法修改客户代码

1
2
3
4
//Penguin 问题:详见软工二
class FlyingBird;
class NonFlyingBird;
virtual void fly() { error("Penguins can't fly!"); } // 如果 penguin:bird 不符合里氏替换原则
契约式设计

每个方法在调用前,应该校验传入参数的正确性,正确才能进行调用,否则是违反契约的。

  1. 前置条件:子类方法相比于父类中部被覆盖方法相同或更加宽松(能调用父类一定能调用子类)
  2. 后置条件:子类方法相比于父类中部被覆盖方法相同或更加严格(调用子类不出错,调用父类一定不出错)
长方形问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// v1
class Rectangle {
public:
void setHeight(int);
void setWidth(int);
int height() const; //不修改的声明为 const 方便常对象调用
int width() const;
};

class Square : public Rectangle {
public:
void setLength(int);

private: //设置为 private 可以避免父类的方法被单独调用
void setHeight(int);
void setWidth(int); //可以单独改变一条边
};
assert(s.width() == s.height()); //前后校验不变式

// 通过基类指针调用函数修改一条变长,违反里氏替换原则
Square s(1, 1);
Rectangle* p = &s;
p->setHeight(10);
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
// v2 添加虚函数声明
class Rectangle {
public:
virtual void setHeight(int);
virtual void setWidth(int);
int height() const;
int width() const;
};

class Square : public Rectangle {
public:
void setLength(int);

public: //注意是 public 的
void setHeight(int);
void setWidth(int); //可以单独改变一条边
};

//问题
//如下的操作如果传入了正方形(对于使用了这一套继承体系的代码)
void Widen(Rectangle& r, int w) {
int oldHeight = r.height();
r.setWidth(r.width() + w); // set 被重写过
assert(r.height() == oldHeight);
}
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
// v3
class Rectangle {
public:
virtual void setHeight(int);
virtual void setWidth(int);
int height() const;
int width() const;
};
assert(s.width() == s.height());
class Square : public Rectangle {
public:
void setLength(int);

private: //修改为 private
//在编译的时候决定,确定函数位置
void setHeight(int);
void setWidth(int); //可以单独改变一条边
};
assert(s.width() == s.height());
void Widen(Rectangle& r, int w) {
int oldHeight = r.height();
r.setWidth(r.width() + w);
//我们直接修改对应方法为 private,这样子调用会直接报错
//问题:访问控制是编译的时候进行处理的,之后运行的时候该怎么调用就怎么调用
// public 和 private 的访问控制是编译时确定的,而不是虚函数运行时决定
//编译的时候检查 rectangle 是 public 的,没有问题,通过。之后调用的时候发现是虚函数,然后从虚函数表能找到 private 的函数,并且调用。
assert(r.height() == oldHeight);
}
  • 访问控制仅仅是检查这个成员时是否属于当前对象声明的静态部分中可访问,和实际运行时时刻无关。
  • 只要通过编译检查,对于 public 和 private 没有任何问题
  • 最终的问题:想要满足里氏替换原则,派生类的条件应该比基类更加弱,而这个是更强
不要定义与继承而来的非虚成员函数同名的成员函数
1
2
3
4
5
6
7
8
9
10
class B {
class D: public B {
public:
void mf();
};
D x;
B* pB = &x;
pB->mf();//B:mf
D* pD = &x;
pD->mf();//D:mf
  1. 对于一个对象,在不同的指针的情况下会表示出来不同的行为,这是很麻烦的。
  2. 运行时不要出现不一致的

private inheritance 私有继承

  1. 特别含义:Implemented-in-term-of 用继承来实现某个功能
    • 需要使用 Base Class 中的 protected 成员,或重载 virtual function
    • 表示不希望一个 Base Class 被 client 使用,否则会使用公有继承
    • 利用一些已经存在的代码,只是在实现中使用到了基类
  2. 在设计层面无意义,只用于实现层面,只是复用实现的方式,接口是被忽略的
  3. 实际上是 Has-A 关系
  4. 如果两个类的继承是私有的,则不能在派生类外将派生类转换成基类对象。

和组合的联系

  1. 尽可能用组合,万不得已用继承

情况一

需要使用 protected 和重载 virtual function

情况二
  1. 我们需要尽可能复用,并且减少内存的占用。
  2. 如果没有静态成员、虚函数、虚基类,那么一个类是被认为是一个空的类,而方法是定义在代码区。对象不占用空间。
  3. 技术上:所有的独立对象必须有一个大小,不然会导致两个指针指向一个地址。
  4. Example:算法的类图,如果使用组合,依据会生成生成一个指针
Example
  1. 目的是为了重定义 eat 函数
  2. 私有继承不能将派生类转换成基类对象
  3. 可以强制转换(提供方法)
1
2
3
4
5
6
class CHumanBeing { … };
class CStudent:private CHumanBeing { … };
void eat(const CHumanBeing & h) { … }
CHumanBeing a;CStudent b;
eat(a);
eat(b);//Error

多继承

定义

  1. 定义
1
2
3
class <派生类名>:[<继承方式>] <基类名 1>,
[<继承方式>] <基类名 2>,…
{<成员表>}
  1. Java 不允许多继承,是因为多继承非常复杂。
  2. 继承方式:默认是 private 的继承方式:public、private 、protected
  3. 继承方式及访问控制的规定同单继承:重复进行继承
  4. 派生类拥有所有基类的所有成员

  • 可以睡的沙发:继承 sofa 和 Bed
  • setWeight 重名:两个基类有相同部分,我们会拆分基类

  • 之后我们拆分出来 setWeigth()(Base Class Decomposition)

  • 形成菱形结构:还有问题,Weigth 变量依旧在,已然有两个 Weigth 部分
  • 解决方案:虚继承

基类声明顺序(初始化顺序)

  1. 基类的声明次序决定:
    1. 对基类构造函数/析构函数的调用次序(顶部基类,同层基类按照声明顺序) 上图中就是 ABCD 的顺序
    2. 对基类数据成员的存储安排
  2. 析构函数正好相反

名冲突

  1. <基类名>::<基类成员名>
  2. 成交变量和成员函数的重名问题:在上图中,D 中会有 B 和 C 的 x
  3. 问题:每次 setWeight 到底是设置谁的?

虚基类

  1. 如果直接基类(如 B、C)有公共的非虚基类(如 A),则该公共基类中的成员变量在多继承的派生类中有多个副本
  2. 如果有一个公共的虚基类,则成员变量只有一个副本
  3. 类 D 有两个 x 成员,B::xC::x
  4. 虚继承:保留一个虚指针
    1. 虚指针指向 A
    2. 可以认为是一个组合关系
  5. 合并
1
2
3
4
5
class A;
class B: virtual public A;
class C: public virtual A;
//public virtual 和 virtual public 是一致的
class D: B, C;

虚基类注意

  1. 虚基类的构造函数由最新派生出的类的构造函数调用
    1. 原来是 B 构造一份 A,C 构造一个 A
    2. 而现在是由 D 调用 A 的构造函数,在 D 的时候先调用 A 的构造函数,在构造 B 和 C 的时候不再调用 A 的构造函数,而只是存放指针
  2. 虚基类的构造函数优先非虚基类的构造函数执行
  3. 如果有两个基类,两个类有一个相同名称的虚函数,比如 B 和 C 都有一个同名的虚函数,到底怎么做?不做要求

操作符重载

函数重载:名同,参数不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Counter {
int value;
public:
Counter() { value = 0; }
Counter& operator ++() { // ++a
value ++;
return *this;
}
Counter operator ++(int) { // a++,int 参数用于区分两个函数,dummy argument,哑元变量
Counter temp = *this;
value++;
return temp;
}
}

不可以重载的操作符: .(成员访问操作符)、.*(成员指针访问运算符,如下)、::(域操作符,后面不是变量)、?:(条件操作符,涉及到跳转,影响理解)、sizeof

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
int x;

public:
A(int i)
: x(i) {}
void f() {}
};

int main() {
void (A::*p_f)();
p_f = &A::f;
A a(1);
(a.*p_f)(); // 通过编译
*p_f(); // 编译错误
return 0;
}

永远不要重载 &&|| :短路运算会造成极大的问题。

注意:=()[]->不可以作为全局函数重载。

  • 大体上来讲,C++ 一个类本身对这几个运算符就已经有了相应的解释了。
  • 如果将这四种符号进行友元全局重载,则会出现一些冲突
  • 下标和箭头运算符为什么?有保留调用顺序,我们希望能保留原来的顺序,而全局不能要求,而成员函数的 this 就可以解决这个问题

操作符重载的哲理:尽量让事情有效率,但不是过度有效率(返回引用),每次就是返回一个拷贝,而不是引用。

操作符 = 的重载

  1. 默认赋值操作符重载函数
  2. 逐个成员赋值
  3. 对含有对象成员的类,该定义是递归的
  4. 赋值操作符的重载不可以被继承:因为拷贝构造,派生出来的类有一些新的部分
  5. 返回引用类型:返回 *this 的引用,支持链式赋值
  6. this 引用应该是非常量引用,返回出来的是作为右值进行计算
    1. a = b = c:不要求非常量引用
    2. (a = b).f():要求非常量引用