永远不要在构造函数和析构函数中调用虚函数

今天在写一个 Android 的 native服务,一不小心就踩到了一个坑,案发现场太复杂,以下是一个简单的 demo:

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
#include <string>
#include <unistd.h>
using namespace std;

class SqliteHelper {
public:
SqliteHelper(const string &dbPath, int dbVersion) {
int version = 0; //get db version from dbPath
if (0 == version) {
onCreate();
}
}
virtual void onCreate() {
printf("SqliteHelper onCreate\n");
}
};

class SystemDB : public SqliteHelper {
public:
SystemDB(const string &dbPath, int dbVersion) : SqliteHelper(dbPath, dbVersion) {

}
virtual void onCreate() {
printf("SystemDB onCreate\n");
}
};

int main() {
SqliteHelper *db = new SystemDB("/data/vendor/pure/db/system.db", 1);
return 0;
}

我们先创建一个数据操作辅助类 SqliteHelper, 其中定义了一个虚函数 virtual void onCreate();
然后我们创建了一个子类 SystemDB 继承了 SqliteHelper, 并覆写了 onCreate 方法。
在 SqliteHelper 的构造函数中判断数据库的版本是不是 0, 如果是 0 的话, 就调用 onCreate 函数去创建数据库表。
这里的 onCreate, 我们预期的应该是子类的实现, 即 SystemDB::onCreate(), 但实际上调用的却是父类的实现。
运行结果如下:

1
2
3
4
/home/deep/projects/clion/cppTest/cmake-build-debug/cppTest
SqliteHelper onCreate

Process finished with exit code 0

一开始没加 log, 加上理所当然的认为调用的会是子类中的实现, 在这里折腾了一个多小时,数据库怎么都创建不出来。
后面慢慢加了 log 才发现这里的坑。查了资料才发现原来 Scott 同学在中早就大力宣传过:
永远不要在构造函数和析构函数中调用虚函数

Item 9: Never call virtual functions during construction or destruction

1、you shouldn’t call virtual functions during construction or destruction, because the calls won’t do what you think, and if they did, you’d still be unhappy. If you’re a recovering Java or C# programmer, pay close attention to this Item, because this is a place where those languages zig, while C++ zags.

2、During base class construction, virtual functions never go down into derived classes. Instead, the object behaves as if it were of the base type. Informally speaking, during base class construction, virtual functions aren’t.

3、It’s actually more fundamental than that. During base class construction of a derived class object, the type of the object is that of the base class. Not only do virtual functions resolve to the base class, but the parts of the language using runtime type information (e.g., dynamic_cast (see Item 27) and typeid) treat the object as a base class type. An object doesn’t become a derived class object until execution of a derived class constructor begins. Upon entry to the base class destructor, the object becomes a base class object, and all parts of C++ — virtual functions, dynamic_casts, etc., — treat it that way.

我个人的理解是:当实例化一个子类对象时,首先进行父类部分的构造,然后再进行子类部分的构造。当在构造父类部分时,子类部分还没开始创建,从某种意义上讲此时它只是个基类对象,所以此时虚函数是基类的函数。
假设在构造函数中虚函数仍然“生效”,即在父类的构造函数中调用的是子类的虚函数。那就有很大的可能会出现以下问题:子类的虚函数使用到子类的成员变量。此时子类的成员变量还没有初始化,就很有可能导致程序异常崩溃了。