一、虚函数的访问控制

虚函数的访问控制(public、protected、private)会影响其在派生类中的重写和调用规则,这是容易混淆的知识点。

1. public 虚函数

基类中 public 的虚函数在派生类中可以被重写为 public 或 protected,但不能重写为 private(在 C++11 前允许,C++11 后被禁止):

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
class Base {

public:

virtual void publicFunc() {

cout << "Base::publicFunc" << endl;

}

};

class Derived : public Base {

public:

// 正确:重写为public

void publicFunc() override {

cout << "Derived::publicFunc" << endl;

}

};

class Derived2 : public Base {

protected:

// 正确:重写为protected

void publicFunc() override {

cout << "Derived2::publicFunc" << endl;

}

};

通过基类指针始终可以调用 public 虚函数,无论派生类将其重写为哪种访问级别:

1
2
3
4
5
6
7
Base* b1 = new Derived();

b1->publicFunc(); // 正确,输出Derived::publicFunc

Base* b2 = new Derived2();

b2->publicFunc(); // 正确,输出Derived2::publicFunc,尽管在Derived2中是protected

2. protected 虚函数

基类中 protected 的虚函数在派生类中可以被重写为 public 或 protected,但不能重写为 private:

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 Base {

protected:

virtual void protectedFunc() {

cout << "Base::protectedFunc" << endl;

}

};

class Derived : public Base {

public:

// 正确:重写为public

void protectedFunc() override {

cout << "Derived::protectedFunc" << endl;

}

};

通过基类指针不能直接调用 protected 虚函数,但可以通过基类的 public 成员函数间接调用:

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 Base {

protected:

virtual void protectedFunc() {

cout << "Base::protectedFunc" << endl;

}

public:

void callProtectedFunc() {

protectedFunc(); // 通过public函数间接调用protected虚函数

}

};

// 使用示例

Base* b = new Derived();

b->callProtectedFunc(); // 正确,输出Derived::protectedFunc

3. private 虚函数

基类中 private 的虚函数在派生类中仍然可以被重写,但派生类无法直接访问基类的 private 虚函数,且通过基类指针也不能直接调用:

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 Base {

private:

virtual void privateFunc() {

cout << "Base::privateFunc" << endl;

}

public:

void callPrivateFunc() {

privateFunc(); // 基类内部可以调用

}

};

class Derived : public Base {

public:

// 正确:重写基类的private虚函数

void privateFunc() override {

cout << "Derived::privateFunc" << endl;

}

};

// 使用示例

Base* b = new Derived();

b->callPrivateFunc(); // 正确,输出Derived::privateFunc

// b->privateFunc(); // 错误,无法直接调用private函数

这种模式非常有用 ——基类控制接口,派生类提供实现,这在模板方法设计模式中经常使用。

二、特殊函数的虚函数特性

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class Base {

public:

// 虚析构函数

virtual ~Base() {

cout << "Base析构函数" << endl;

}

};

class Derived : public Base {

private:

int* data;

public:

Derived() {

data = new int[10];

}

// 自动成为虚析构函数

~Derived() override {

delete[] data;

cout << "Derived析构函数" << endl;

}

};

// 正确使用

Base* obj = new Derived();

delete obj;

// 输出:

// Derived析构函数

// Base析构函数

如果析构函数不是虚函数,删除基类指针时只会调用基类的析构函数,导致派生类的资源无法释放,造成内存泄漏。

2. 构造函数不能是虚函数

与析构函数不同,构造函数不能被声明为虚函数。原因很简单:

  1. 构造函数的作用是初始化对象,包括设置 vptr。在对象构造完成前,vptr 还未完全初始化,无法进行动态绑定。
  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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Base {

public:

Base() {

cout << "Base构造函数" << endl;

print(); // 调用Base::print(),不会调用派生类版本

}

virtual void print() {

cout << "Base::print()" << endl;

}

};

class Derived : public Base {

public:

Derived() {

cout << "Derived构造函数" << endl;

}

void print() override {

cout << "Derived::print()" << endl;

}

};

// 输出:

// Base构造函数

// Base::print()

// Derived构造函数