- 虚函数
- 多态
- 虚函数的析构函数
- 纯虚函数
- override 和 final
- 总结
这并不是常规的虚函数介绍,如果没有了解C++继承,还请移步C++继承,否则下面地内容可能会有些困难
虚函数
接下来,看看虚函数是什么样的吧
虚函数一旦被声明,则必须有定义,否则链接时会报错,除非不实例化这个类。
一般来说,如果我们没有使用到一个函数,则无需定义它。
但虚函数最终会以一个函数指针的方式存在,仅在调用时将其取出。
因为函数指针是一个变量,所以编译器也不清楚是否会使用到某个虚函数(编译器难以预测变量的值,也就是函数指针的值),所以我们必须为每一个虚函数提供定义。
虚函数的声明:
virtual type func();
虚函数会被类存放入虚函数表,并将虚函数表放置在类的开头的位置。
下面我们来验证虚函数表的存在
typedef void func(void); //要与虚函数的函数类型对应
typedef int DWORD; //习惯把要看作十六进制的int 写作 DWORD
class Animal {
public:
virtual void move() { cout << "动物在动" << endl; }
virtual void breath() { cout << "动物在呼吸" << endl; }
};
void main() {
Animal a;
DWORD* aThis = (DWORD*)&a;
DWORD** pVirtualFuncTable = (DWORD**)(*aThis);//等会还要运算所以先写成int
//func* pVirtualFuncTable[] = a.(*this);//这相当于这样的声明
//DWORD** pVirtualFuncTable = *(DWORD***)&a; //上面两句的替代写法,能理解一种就行
//为什么写出*(DWORD***)&a这样的代码,主要是我们想将 a 转换成DWORD**,但系统不让写(DWORD**)a
//第一种调用方式,常规写法
a.move();
a.breath();
//第二种调用方式
((func*)*pVirtualFuncTable)();//(func*)pVirtualFuncTable[0]; //DWORD* --> func*
((func*)*(pVirtualFuncTable + 1))();//(func*)pVirtualFuncTable[1]
}
//打印结果:
//动物在动
//动物在呼吸
//动物在动
//动物在呼吸
两种方法都能正确调用虚函数
我们可以聚焦到第二种调用方法上,我们采用一个DWORD*
也就是int*
来读取a.this
。
接着我们对aThis
进行了解引用,并将解引用的值当做一个指针转存起来,起名pVirtualFuncTable(虚函数表)
。
其实可以想象这是一个结构体,结构体的第一个元素存放的是一个数组,而我们获取的是这个数组的首地址,所以pVirtualFuncTable
所指向的就是虚函数数组的首地址,这个数组里存放的都是函数的地址。
struct cls{
int addrs[n];
...
}
虚函数表里的内容是函数指针,也就是说*pVirtualFuncTable
是一个函数指针(func*
,但是我们暂时把它写成了DWORD*
),可以简单理解成func* pVirtualFuncTable[]
,*pVirtualFuncTable
相当于pVirtualFuncTable[0]
,能取出一个函数指针func*
,但编译器也不会让我们写arr[] = &x
这样的代码,所以只能写成上面的那种形式。
最后,我们将这个DWORD*
转换成func*
,并对其进行调用。
虽然类的实例不能直接转换成DWORD*
,但是可以写(int*)0x1234
,这样的代码,但是为了便于理解,我还是写成了func* arr[]
这样的形式。
如果声明成func arr[]
(也就是DWORD*
),那么会少一级指针,最后也能让DWORD
成功转换成func*
那么有的同学就要问了,如果我实例化多个类的时候,虚函数表会是同一个吗?
废话不多说,直接写代码好吧
void main(){
Animal a1, a2;
DWORD* pVirtualFuncTable1 = *(DWORD**)&a1;
DWORD* pVirtualFuncTable2 = *(DWORD**)&a2;
cout << (pVirtualFuncTable1 == pVirtualFuncTable2) << endl;
}//打印结果: true
确实,他们会指向同一个虚函数表
上面那个小实验的补充内容:(可选则跳过这段内容)
有的人可能会好奇到打印一下函数的第一条指令是不是一样的,但是很遗憾,是不一样的。
如果对这个问题不感兴趣,那么就跳过吧。
int main(){
...
//尝试打印这些值,但是很遗憾,每个函数指针打印出来的值并不能很好地对应
//需要把 跳转表 关掉才行,捣鼓了半天,也没搞懂怎么关
printf("%p\t", &Animal::move);
printf("%p\n", &Animal::breath);
printf("%p\t", *pVirtualFuncTable);
printf("%p\n", *(pVirtualFuncTable + 1));
}
这一段打印代码不能打印出一样的地址的原因如下:
这里涉及到跳转表,函数调用时首先会跳掉一个全是jmp指令
的地方,然后才跳到函数开始的地方。
然而每次调用的时候,都是不同的地址,然而都会跳到同一个地方去执行,这意味着两个跳转表都能跳到正确的位置执行,有兴趣的同学可以自己想办法关掉跳转表,再打印
多态
首先回顾一下,之前所学的内容
之前说过,如果出现同名函数,那么重名的函数会被重写,但是并不会将其删除,而是被隐藏起来了。
所以我们会优先调用当前类型的同名函数,People* p = new People
,那么,当前类型就是People
如何实现多态?
当我们对一个虚函数进行重写的时候,就能实现多态
class Animal {
public:
virtual void move() { cout << "动物在动" << endl; }
virtual void breath() { cout << "动物在呼吸" << endl; }
};
class Bird : public Animal {
public:
void move() { cout << "鸟儿在飞" << endl; }
void breath() { cout << "鸟儿在空中呼吸" << endl; }
};
void main() {
Animal* a = new Animal;
Animal* a2 = new Bird;
a->move();
a->breath();
a2->move();
a2->breath();
delete a;
delete a2;
}
//打印结果:
//动物在动
//动物在呼吸
//鸟儿在飞
//鸟儿在空中呼吸
如果没有写virtual关键字的话,声明是什么类的变量,就会调用哪个类的函数。
Animal a
不管后面写的是什么,如果函数不是virtual的,那么就会调用Animal::func()
。
如果声明了virtual的,那么就会按照定义时(new)的类型进行调用。
到底是什么使得virtual,能调用new的类的函数呢?
之前我们说过了,virtual的函数会装入一个虚函数表里。
那么有没有可能是在new的时候,就直接把自己的虚函数写入了这个表呢?
下面我们来试着验证一下:
稍微修改忆点点main函数
void main() {
Animal* a = new Animal;
Animal* a2 = new Bird;
Bird* b = new Bird;
DWORD** a_virTable = *(DWORD***)a;
DWORD** a2_virTable = *(DWORD***)a2;
DWORD** b_virTable = *(DWORD***)b;
//func* virTable[] = *(DWORD***)a; //这样的写法是错误的,只是便于理解
printf("a point to move:\t %p\n", *a_virTable); //打印函数指针
printf("a point to breath:\t %p\n", *(a_virTable + 1));
printf("a2 point to move:\t %p\n", *a2_virTable);
printf("a2 point to breath:\t %p\n", *(a2_virTable + 1));
printf("b point to move:\t %p\n", *b_virTable);
printf("b point to breath:\t %p\n", *(b_virTable + 1));
((func*)*a_virTable)(); //a->move();
((func*)*(a_virTable + 1))(); //a->breath();
((func*)*a2_virTable)(); //a2->move();
((func*)*(a2_virTable + 1))(); //a2->breath();
((func*)*b_virTable)(); //b->move();
((func*)*(b_virTable + 1))(); //b->breath();
delete a;
delete a2;
delete b;
}
//打印结果:
//a point to move: 010E1537
//a point to breath: 010E1762
//a2 point to move: 010E1609
//a2 point to breath: 010E1992
//b point to move: 010E1609
//b point to breath: 010E1992
我们发现,正如猜想的那样,a2
的虚函数表的内容和b
的一模一样。
如果虚函数没有被重写,那么会装载父类的虚函数,这和继承是一样的。
可以自己验证一下。
书上把填充虚函数表这个行为称之为绑定,但我们现在只看结果,不用去在意过程是怎么样的,不用关注具体到在什么时候进行的绑定,除非真的到了要了解这个的时候。
我们只需要大概了解到虚函数在new的时候会将虚函数填充完整, 如果遇到了没有重载的虚函数,那么就会将父类的虚函数填入表中,如果父类也没有向上进行寻找,直到没有父类为之。
虚函数的析构函数
上次谈到使用多态形式时,delete 实例
只会调用当前类型的析构函数,因为子类的空间并不归父类管
那么想要调用子类的析构函数,就要另辟蹊径了。
上面我们讲到,如果函数被声明为虚函数,那么实例化(new)的类型的对应函数的函数指针就会被装载到虚函数表中。
不管当前类是什么,调用的都是这个虚函数表中的虚函数的指针。
那么如果我们把析构函数声明成虚函数,new的时候析构函数就会装载到虚函数表里,我们再调用这个类的析构函数的时候,就会查虚函数表,刚好能找到创建时放入的函数。
而子类的析构函数会调用父类的析构函数。
这一波简直完美。
class Animal {
public:
virtual ~Animal() { cout << "Animal 析构函数" << endl; }
};
class Bird : public Animal {
public:
~Bird() { cout << "Bird析构函数" << endl; }
};
int main(){
Animal* b = new Bird;
delete b;
}
//打印结果:
//Bird析构函数
//Animal 析构函数
纯虚函数
有纯虚函数的类被当做是模板类(抽象类)。
这个类不能被实例化,因为纯虚函数并没有定义(实现),而之前提到虚函数必须要有定义。
纯虚函数的声明:
virtual void func() = 0;
没错,就是显示地写出 = 0,表示我不回去实例化它,如果要在声明纯虚函数的作用域定义它,会被编译器拒绝。
这样就能防止不小心定义了这个纯虚函数。
继承的父类中有纯虚函数,但不实例化,那么当前类也被认为是个模板类。
override关键字的主要作用是防止我们重载写错的
试想一下,我在父类中写了一个函数virtual void func(w_char c)
,然后我的子类继承了父类,我要重写这个函数。
但是一不小心我写错了,写成(virtual) void func(char c)
。
但编译器并不会报错,因为这是个全新的函数,我们为子类声明一个新的函数,或是一个新的虚函数,这并没有什么不妥,但这个结果却不是我们所想要的。
所以为了防止这种错误的发生,C++11起,可以使用override
关键字,表示这个函数是被重写的。
当我们写void func(char c) override
的时候,编译器会查找父类,父类的父类…如果没有找到,那么他就会报错,因为这个函数匹配不到可重写的函数,这是个新的函数,不符合override
的语义
而final
则更为简单,字面上的意思是最后的
所以如果这个关键字写在函数后面,则表示我不希望子类再对这个函数进行重写了,这是最后的版本。
如果写在类的后面,则表示这个类是最终版本,不希望再被继承。
- 虚函数表会被放置在类空间的最上方的位置
- 当new的时候,函数会将虚函数表填充完整
- 当子类虚函数没有被重载的时候,那么会想父类进行查找填充
- 如果父类,父类的父类…查到尽头都没有查到虚函数的定义,那么这个函数不能被实例化
- 模板类(抽象类)不能被实例化,如果子类继承了抽象类,但没有重写纯虚函数,那么子类也是模板类,也不能被实例化
- 善用override关键字,也许会在关键时刻省去许多麻烦,final表示这个最后一个版本
- 对了,还有跳转表是个有意思的东西,函数不会直接call到函数本体里去,而是通过一个跳转表重新定位函数所在的地方
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)