20200916 effective cpp 02 virtual使用注意事项

前言

virtual基本是面试必面的知识点了。我们对virtual的认知就只是「想要实现多态,就得用它」。当然这没错,但是virtual的使用还有许许多多的事项和坑点需要注意的。

代替方案

首先考虑,我们非得用多态吗?

书中给了另外一种方案:策略模式。

所谓策略模式,简而言之,不同实例可以有不同的策略。比如,对于武器伤害,有一种伤害方式为直接掉血,另一种伤害方式则是给被伤害者附加一个debuff,那么我们可以传入一个伤害策略给武器,从而实现不同的武器伤害效果。

对于C++,我们可以通过传入函数指针来实现。

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
#include <funtional.h>

void DefualtHurtStrategy() { }

class Weapon
{
public:
Weapon():
_HurtStrategy(DefualtHurtStrategy)
{}

SetHurtStrategy(function<void (UObject*)> HurtStrategy) { _HurtStrategy = HurtStrategy; }

public:
Hurt(UObject* Target)
{
_HurtStrategy(Target);
}
}

void DoDamageStrategy(UOBject* Target)
{
// 掉血
}

void AddDebuffStrategy(UOBject* Target)
{
// 添加debuff
}

int main()
{
// 一把有伤害效果的武器
Weapon* pExcalibur = new Weapon();
pExcalibur->SetHurtStrategy(DoDamageStrategy);

// 一把添加
Weapon* pCaladbolg = new Weapon();
pCaladbolg->SetHurtStrategy(AddDebuffStrategy);
}

当然,通过使用std::bind + std::function,我们也可以让成员函数成为策略。

坑点:默认参数的静态绑定

结论先行:

如果派生类重新定义了虚函数,并且改变了参数的默认值,使用的时候可能会有意料不到的结果。简单来说,当静态类型(持有类型)是基类的时候,会使用基类的默认参数,当静态类型是派生类的时候,会使用派生类的默认参数。

这是由于:virtual的函数是动态绑定的,但是其默认参数却是静态绑定的。

我们看看下面的代码:

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
using namespace std;

class Painter
{
public:
virtual void Draw(int Color = 1)
{
PrintColor(Color);
}

void PrintColor(int Color)
{
cout << "Color: " << Color << endl;
}
};

class PenPainter : public Painter
{
public:
virtual void Draw(int Color = 2)
{
PrintColor(Color);
}
};

int main() {
PenPainter* pDerivedTypeInstance = new PenPainter();
pDerivedTypeInstance->Draw();

Painter* pBaseTypeInstance = pDerivedTypeInstance;
pBaseTypeInstance->Draw();


system("pause");
}

输出:

1
2
Color: 2
Color: 1

解决方案

很简单,virtual函数不要带参数默认值。一旦这么做了,继承者也就没有办法更改参数默认值了,毕竟一开始基类虚函数就不存在参数默认值(笑)。

如果我们需求上还是需要参数的默认值,解决方法是新建一个non-virtual函数,让它来做参数默认值,然后再把这个值传给virtual方法,比如我们可以把上面的例子改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Painter
{
public:
void Draw(int Color = 1)
{
DoDraw(Color);
}
protected:
virtual void DoDraw(int Color); // 这里的虚函数就不要带默认参数了
}

class PenPainter
{
protected:
virtual void DoDraw(int Color);
}

坑点:构造函数调用虚函数

结论先行:

在基类的构造和析构函数中调用虚函数,即使虚函数被派生类重写了,调用的也永远会是基类版本。

一句话解释原理:构造和析构函数的时候,没有多态。

同样举上面Painter的例子:

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 Painter
{
public:
Painter()
{
PrintColor(1);
}

virtual void PrintColor(int Color)
{
cout << "Base Color: " << Color << endl;
}
};

class PenPainter : public Painter
{
public:
virtual void PrintColor(int Color)
{
cout << "Pen Color: " << Color << endl;
}
};

int main() {
PenPainter* pDerivedTypeInstance = new PenPainter();
system("pause");
}

输出:

1
Base Color: 1

除此之外,还要注意另一种比较隐蔽的坑,那就是构造函数或析构函数中间接调用虚函数。同样的,没有多态,不讲道理。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Painter
{
public:
Painter()
{
InitPainter();
}

/* 这个不是虚函数,但是调用了虚函数 */
void InitPainter()
{
Foo();
}

/* 这个是虚函数 */
virtual void Foo();
}

解决方案

不要使用virtual。

用传入参数解决

我们使用virtual不就是想要不同的逻辑?如果不同的逻辑是由某个变量决定的,那么就把这个变量变成函数的参数来传入,把函数变回non-virtual。

1
void Painter::Foo(string Message);

用策略模式

跟前文提到的一样,想要实现不同的功能,可以由外界传入一个策略来解决。

1
2
3
4
5
6
7
8
9
10
11
12
class Painter
{
private:
function<void()> _InitFunc;

public:
Painter(function<void()> InitFunc)
{
_InitFunc = InitFunc;
_InitFunc();
}
}
Buy Me A Coffee / 捐一杯咖啡的钱
分享这篇文章~
0%
//