虚函数详解

定义

多态(polymorphism)是面向对象编程语言的一大特点,而虚函数是实现多态的机制。其核心理念是通过基类访问派生类定义的函数。

多态性使得程序调用的函数是在运行时动态确定的,而不是在编译时静态确定。

  • 虚函数。在类成员方法的声明语句前加上virtual关键字,例如:virtual void func();

  • 纯虚函数。在虚函数后加上"=0",如virtual void func() = 0;

对于虚函数,子类可以(也可以不)重新定义基类的虚函数,该行为称为复写(override)

虚函数

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
// no_virtual.cpp

#include <iostream>
#include <string>

// 宠物类
class Pet {
public:
std::string GetName() { return "pet"; }
};

// 狗类
class Dog : public Pet {
private:
std::string m_Name;
public:
Dog(const std::string& name)
: m_Name(name) {}

std::string GetName() { return m_Name; }
};

int main() {
Pet* e = new Pet();
std::cout << e->GetName() << std::endl;

Dog* Peter = new Dog("Peter");
std::cout << Peter->GetName() << std::endl;

return 0;
}

编译执行:

1
2
g++ -o test.o no_virtual.cpp
./test.o

输出:

pet

Peter

这和我们想的一样,如果我们使用多态的概念来编写上面的代码:

在main里面加上:

1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
Pet* e = new Pet();
std::cout << e->GetName() << std::endl;

Dog* Peter = new Dog("Peter");
std::cout << Peter->GetName() << std::endl;

// 我们希望输出“Peter”
Pet* pet = Peter;
std::cout << pet->GetName() << std::endl;

return 0;
}

输出:

pet

如果我们这样编写:

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
#include <iostream>
#include <string>

// 宠物类
class Pet {
public:
std::string GetName() { return "pet"; }
};

// 狗类
class Dog : public Pet {
private:
std::string m_Name;
public:
Dog(const std::string& name)
: m_Name(name) {}

std::string GetName() { return m_Name; }
};


void PrintName(Pet* pet) {
std::cout << pet->GetName() << std::endl;
}

int main() {
Pet* e = new Pet();
PrintName(e);

Dog* Peter = new Dog("Peter");
PrintName(Peter);
return 0;
}

我们期望输出的是:pet Peter

输出:

pet

pet

这是因为到了调用方法时,都会调用这个类型的方法,因为PeintName这个函数是属于Pet类型的指针,调用这个函数,将会调用Pet中的getName

如果我们希望C++可以根据我们传入的类型不同自动来选择对应的函数,这时就需要用到虚函数了。

虚函数引入了动态分配,使用虚函数表(VTable)来实现编译,虚函数表包含基类中所有虚函数的映射,以便我们能在运行时映射它们到正确的覆写函数。

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
#include <iostream>
#include <string>

// 宠物类
class Pet {
public:
// 定义虚函数
virtual std::string GetName() { return "pet"; }
};

// 狗类
class Dog : public Pet {
private:
std::string m_Name;
public:
Dog(const std::string& name)
: m_Name(name) {}
// 覆写
std::string GetName() override { return m_Name; }
};


void PrintName(Pet* pet) {
std::cout << pet->GetName() << std::endl;
}

int main() {
Pet* e = new Pet();
PrintName(e);

Dog* Peter = new Dog("Peter");
PrintName(Peter);
return 0;
}

这时就能输出:pet Peter

需要特别说明下override是C++11新特性,我们可以加上也可以不加上,不过最好还是加上,这可以避免我们覆写基类函数中没有定义的函数。

纯虚函数

在很多情况下,基类本身生成对象是不合理的,例如,宠物Pet作为一个基类可以派生出猫,狗等子类。

纯虚函数是在基类中声明的虚函数,它要求任何派生类都要定义自己的实现方法,以实现多态性。实现了纯虚函数的子类,该纯虚函数在子类中就 变成了虚函数。

定义纯虚函数是为了实现一个接口,用来规范派生类的行为。

含有纯虚函数的类称为抽象类,它不能生成对象(创建实例),只能创建它的派生类的实例。

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
// pure_virtual.cpp

#include <iostream>
#include <string>

// 宠物类
class Pet {
public:
// 定义纯虚函数
virtual std::string GetName() = 0;
};

// 狗类
class Dog : public Pet {
private:
std::string m_Name;
public:
Dog(const std::string& name)
: m_Name(name) {}
// 覆写
std::string GetName() override { return m_Name; }
};


void PrintName(Pet* pet) {
std::cout << pet->GetName() << std::endl;
}

int main() {
Pet* e = new Pet(); // error 抽象类不能创建实例
PrintName(e);

Dog* Peter = new Dog("Peter");
PrintName(Peter);
return 0;
}

改进过后:

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
52
53
// pure_virtual.cpp

#include <iostream>
#include <string>


class Printable {
public:
// 纯虚函数
virtual std::string GetClassName() = 0;
};

// 宠物类
class Pet : public Printable {
public:
// 定义虚函数
virtual std::string GetName() { return "pet"; }
std::string GetClassName() override { return "Pet"; }

};

// 狗类
class Dog : public Pet {
private:
std::string m_Name;
public:
Dog(const std::string& name)
: m_Name(name) {}
// 覆写
std::string GetName() override { return m_Name; }
std::string GetClassName() override { return "Dog"; }
};


void PrintName(Pet* pet) {
std::cout << pet->GetName() << std::endl;
}

void Print(Printable* obj) {
std::cout << obj->GetClassName() << std::endl;
}

int main() {
Pet* e = new Pet();
// PrintName(e);

Dog* Peter = new Dog("Peter");
// PrintName(Peter);

Print(e);
Print(Peter);
return 0;
}

输出:

Pet

Dog

对于Print函数,接受printable对象,它不关心实现的是什么类(多态性)。

如果你没有实现GetClassName这个函数,你就不能实例化这个类

1
2
3
4
5
6
7
class A : Printable {
std::string GetClassName() override { return "A"; }
}

int main() {
Print(new A()); // 只有实现了GetClassName函数才能实例化这个类
}

注意

静态函数可以声明为虚函数吗?

静态函数不可以声明为虚函数,同时也不能被constvolatile关键字修饰

static成员函数不属于任何类对象或类实例,所以给此函数加上virtual也是没有意义的。

虚函数依靠vptrvtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,静态成员函数没有this指针,所以 无法访问vptr

构造函数可以为虚函数吗?

构造函数不可以声明为虚函数,同时除了inline,explicit之外,构造函数不允许使用其他任何关键字。

尽管虚函数表vtable是在编译阶段已经建立的,但指向虚函数表的指针vptr是在运行阶段实例化对象时才产生的。如果类含有虚函数,编译器会在构造函数中 添加代码来创建vptr。问题来了,如果构造函数是虚的,那么它需要vptr来访问vtable,可这个时候vptr还没产生。因此,构造函数不可以为虚函数。

我们之所以使用虚函数,是因为需要在信息不全的情况下进行多态运行。而构造函数是用来初始化实例的,实例的类型是必须明确的。因此,构造函数没有 必要被声明为虚函数。

析构函数可以为虚函数吗?

析构函数可以声明为虚函数。

虚函数可以被内联吗?

  • 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表示多态性的时候不能被内联。

内联是在编译期间,而虚函数的多态性是在运行期,编译器无法知道运行期间调用哪个代码,因此虚函数表现为多态时,不可以被内联。