虚函数
面向对象语言,有三个最主要的特性,继承,封装,多态。其中多态是实现就是依仗虚函数来实现的。
所谓多态实际上就是子类对象指针赋值给父类指针,在运行时确定具体的对象类型。
如下所示:
我们定义了两个类如下: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
33class Father {
public:
Father(std::string name, int age) : m_name(name), m_age(age) {};
~Father() = default;
void SetName(std::string name) {
m_name = name;
}
void SetAge(int age) {
m_age = age;
}
std::string GetName() { return m_name; }
int GetAge() { return m_age; }
private:
std::string m_name;
int m_age;
};
class Son : Father {
public:
Son(std::string degree, std::string name, int age) : Father(name, age), m_degree(degree) {};
~Son() = default;
void SetDegree(std::string degree) {
m_degree = degree;
}
std::string GetName() { return m_degree; }
private:
std::string m_degree;
};
我们可以分别创建父类和子类的对象来进行操作。
而当我们同一个父类指针指向不同的子类对象时,利用虚函数,可以让其执行具体子类的方法,如下所示: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
51class Father {
public:
Father(std::string name, int age) : m_name(name), m_age(age) {};
~Father() = default;
void SetName(std::string name) {
m_name = name;
}
void SetAge(int age) {
m_age = age;
}
std::string GetName() { return m_name; }
int GetAge() { return m_age; }
virtual std::string GetDescription() {
return "father";
}
private:
std::string m_name;
int m_age;
};
class Son : public Father {
public:
Son(std::string degree, std::string name, int age) : Father(name, age), m_degree(degree) {};
~Son() = default;
void SetDegree(std::string degree) {
m_degree = degree;
}
std::string GetDegree() { return m_degree; }
std::string GetDescription() {
return "son";
}
private:
std::string m_degree;
};
class Son1 : public Father {
public:
Son1(std::string name, int age) : Father(name, age) {};
~Son1() = default;
std::string GetDescription() {
return "son1";
}
};
1 | int main() |
输出结果如下:1
2son
son1
纯虚函数
格式如下的函数被称为纯虚函数
virtual void GetName() = 0;
纯虚函数一般用来做接口类(因为C++不像JAVA有接口),接口类是不能实例化的类。
纯虚函数需要在子类中实现。
1 | class Person { |
虚表
虚函数是多态的根基。那么为什么利用虚函数就能够实现多态呢?也就是为什么父类指针能够知道自己到底是指向的那个子类呢?这就是虚表的功效。
每个包含了虚函数的类都包含有一个虚表,虚表是一个指针数组,其元素是虚函数的指针。对于普通的非虚函数则不在虚表里管理。一个类有一个虚表。虚表是属于类的,不属于类的具体对象,同一个类的所有对象公用一个虚表。
动态绑定
C++是如何利用虚表去实现动态绑定的呢?我们以下面的例子分析:
假设我们有如下的类关系:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
class B : public A {
public:
virtual void vfunc1();
void func1();
private:
int m_data3;
};
class C: public B {
public:
virtual void vfunc2();
void func2();
private:
int m_data1, m_data4;
};
那么三个类的虚表与相应的虚函数管理如下所示:
对于A类,它有一个虚表指针vbtlA,它指向了A中的的虚函数vfunc1()和vfunc2()。
对于B类,它也有一个虚表指针vbtlB,它指向了B中的虚函数vfunc1()和继承自A中的虚函数vfunc2()
对于C类,它也有一个虚表指针vbtlC,它指向了C中的虚函数vfunc2()和继承自B中的虚函数vfunc1()
这样在运行时,通过虚表就能清晰的分辨出具体应该调用那个函数了。
虚函数的性能
我们可以看到,引入了虚函数,就多了虚表的跳转的这一步骤。但是实际上这个步骤对性能的影响实际非常小。真正影响到性能的根源在于编译器在编译时通常并不知道它将要调用哪个函数,所以它不能被内联优化和其它很多优化,因此就会增加很多无意义的指令(准备寄存器、调用函数、保存状态等),而且如果虚函数有很多实现方法,那分支预测的成功率也会降低很多,分支预测错误也会导致程序性能下降。
不能为虚函数的函数
构造函数不能为虚函数
这是因为new一个对象的时候,需要知道对象的实际类型,而虚函数是在运行时才能确定类型的。
同样的虚函数依赖虚表,而虚表也是要在构造函数中初始化的。
析构函数为什么会设置为虚函数
析构函数设置为虚函数是为了防止内存泄漏。因为当我们用基类指针指向派生类时,如果析构函数不是virtual的,delete基类指针,则只会析构掉基类的部分,没有办法析构派生类相对于基类新增加的部分。如果析构函数为虚函数,那么delete基类指针则会直接调用派生类的析构函数,就不会存在上面的问题。
内联函数不能为虚函数
因为内联函数在编译时会进行代码展开,编译器在函数声明中遇到virtual关键字时,会选择忽略inline关键字,不进行代码展开。
静态函数不能为虚函数
虚函数体现了对象在运行时的多态性,而静态函数属于整个类,不属于某个对象,不能声明为虚函数。