C++语法之虚函数、虚表

本文首发于个人博客

虚函数

  • C++中的多态通过虚函数(virtual function)来实现
    • 虚函数:被virtual修饰的成员函数
    • 只要在父类中声明为虚函数,子类中重写的函数也自动变成虚函数(也就是说子类中可以省略virtual关键字)

先看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person{
public:
int age;
void run(){
cout<< "Person::run()" <<endl;
}
};

class Student: public Person{
public:
int height;
void run(){
cout<< "Student::run()" <<endl;
}
};

我们用Person指针指向Student对象,然后真正输出的是Person::run()

1
2
3
4
5
int main(){
Person *stu = new Student();
stu->run(); //输出 Person::run()
return 0;
}

输出

Person::run()

原因

  • 那是因为对于编译器来说,编译代码的时候,发现stu指针是Person类型,那么调用的时候,直接调用了Person::run()。有没有办法调用Student::run()呢?答案是有的,就是用virtual修饰
  • virtual修饰的函数,是虚函数
1
2
3
4
5
6
7
class Person{
public:
int age;
virtual void run(){
cout<< "Person::run()" <<endl;
}
};

改成如上代码之后,输出结果为

Student::run()

虚表

  • 虚函数的实现原理是虚表,这个虚表里面存储着最终需要调用的虚函数地址,这个虚表也叫虚函数表

对于上面的例子中,Student对象的前4个字节存放的是指向虚表的地址
当我们调用的时候,

1
2
Person *stu = new Student();
stu->run();

会首先把虚表地址取出来,然后去虚表中调用Student::run()

所有的Student对象(不管在全局区、栈、堆)共用同一份虚表

1
2
3
4
5
6
0x1000010e5 <+53>: movq   -0x18(%rbp), %rax
0x1000010e9 <+57>: movq %rax, -0x10(%rbp)
0x1000010ed <+61>: movq -0x10(%rbp), %rax
0x1000010f1 <+65>: movq (%rax), %rdx
0x1000010f4 <+68>: movq %rax, %rdi
0x1000010f7 <+71>: callq *(%rdx) //rdx里面存放虚表地址

跟踪汇编代码调用如下的Student::run:

1
2
3
4
5
6
7
8
9
 C++test01`Student::run:
-> 0x100001190 <+0>: pushq %rbp
0x100001191 <+1>: movq %rsp, %rbp
0x100001194 <+4>: subq $0x10, %rsp
0x100001198 <+8>: movq 0xe61(%rip), %rax ; (void *)0x00007fff97678760: std::__1::cout
0x10000119f <+15>: movq %rdi, -0x8(%rbp)
0x1000011a3 <+19>: movq %rax, %rdi
0x1000011a6 <+22>: leaq 0xda6(%rip), %rsi ; "Student::run()"
0x1000011ad <+29>: callq 0x100001de2 ; symbol stub for: std::__1::basic_ostream<char, std::__1::char_traits<char> >& std::__1::operator<<<std::__1::char_traits<char> >(std::__1::basic_ostream<char, std::__1::char_traits<char> >&, char const*)

纯虚函数

  • 纯虚函数:没有函数体且初始化为0的虚函数,用来定义接口规范
  • 抽象类(Abstract Class)
    • 含有纯虚函数的类,不可以实例化(不可以创建对象)
    • 抽象类也可以包含非纯虚函数、成员变量
    • 如果父类是抽象类,子类没有完全重写纯虚函数,那么这个子类依然是抽象类

例如下面的Person类就是一个含有纯虚函数的类,可以用来定义接口规范

1
2
3
class Person{
virtual void run()=0;
};
  • 定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
  • 纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。
  • 所以类纯虚函数的声明就是在告诉子类的设计者,”你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。