C++虚函数的一点分析与思考
简介:
以下是自己看过的书籍以及自己思考的流程和总结,主要是对C++虚函数分析了,分析并不算足够深入,但相信对理解c++的虚函数会有些帮助。现在仅仅写到了单继承下的一些皮毛,后面还要继续挖掘一下,希望自己能以淡定一点的心做好一块,不负自己。
以下内容适合了解一些C++虚函数以及对指针操作相对来说有点基础的朋友,因为里面为了验证自己的思考进行了很多指针的强转,下面的测试需要有实际的代码操作,不希望大家仅仅看结论就算完了,那很没有意思。我在本地建了一个vs工程,先从最简单的测试做起,然后一点点的加入代码.一点点的深入,代码写的比较乱,仅仅是为了自己的测试,望朋友包涵一下了。
代码如下:
Shape.h
log.h
typedef.h
main.cpp
Shape.h:
#ifndef __SHAPE__HEAD_H
#define __SHAPE__HEAD_H
#include "typedef.h"
#include "log.h"
class CShape
{
public:
CShape(){
TRACE_FUCTION_AND_LINE("");
m_color = 15;
}
~CShape(){
TRACE_FUCTION_AND_LINE("");
}
void SetColor(int colore){
TRACE_FUCTION_AND_LINE("");
m_color = colore;
}
protected:
private:
int m_color;
};
class CRect : public CShape
{
public:
CRect(){TRACE_FUCTION_AND_LINE(""); m_width = 0; m_height = 255;}
~CRect(){TRACE_FUCTION_AND_LINE("");}
void PrintMemory(){
TRACE_FUCTION_AND_LINE("this: %p", this);
int *p = (int*)this;
TRACE_FUCTION_AND_LINE("4: %d", *p);
TRACE_FUCTION_AND_LINE("4: %d", *(p+1));
TRACE_FUCTION_AND_LINE("4: %d", *(p+2));
}
protected:
private:
int m_width;
int m_height;
};
#endif
log.h:
#define TRACE_FUCTION_AND_LINE(fmt, ...) printf("[%30s:%4d] "fmt"\n",__FUNCTION__, __LINE__, ##__VA_ARGS__)
typedef.h:
仅仅是一些跨平台的宏定义,就不列出来了,针对我们的问题不起主要作用。
main.cpp:
#include <iostream>
using namespace std;
#include "Shape.h"
int main()
{
CRect rect1;
TRACE_FUCTION_AND_LINE("sizeof(CShape):%d", sizeof(CShape));
TRACE_FUCTION_AND_LINE("sizeof(CRect):%d, %p", sizeof(CRect), &rect1);
rect1.PrintMemory();
rect1.SetColor(10);
rect1.PrintMemory();
return 0;
}
问题1:
派生类和基类的内存如何排布?
通过main.cpp中rect1的打印内存的操作我们可以看出,派生类占用12字节内存,分别是基类的m_color,以及自己的两个int成员。
基类占有4个字节的内存,SetColor函数本身不占用任何内存。
真理:对象所占用内存包含3个部分,非静态数据成员(自身的和父类的),vptr(后面介绍),字节对齐。
因此不要武断的说,c++占用内存比C多,其实就一个vptr的问题,字节对齐在结构体中同样会出现,字节对齐对上层来说是透明的,因此不用太过于例会。
派生类如何调用基类的非虚public函数,例如本例的SetColor?
1,对于SetColor方式,编译器会将其编译成SetColor(int colore, CShape* pShape); rect进行调用的时候采用的纯C的方式,也就没有任何多余的开销。
rect1.SetColor(color)将会被展开为 SetColor(color, &rect1); 于是rect1的地址被传入到pShape中,继而调用pShape->m_color给m_color赋值。
对于编译器来说,它只看到传过来的参数地址为&rect1, 它并不知道它的实际类型是什么,对于任何类型都将会被SetColor强转为CShape的类型,于是这就引出一个问题,编译器怎么知道实际的rect1的m_color地址偏移是多少呢?实际上它压根就不知道它是CRect类型.在SetColor这个函数指令运行的时候,它仅仅是根据传入pShape的地址,以CShape的方式偏移特定的地址(对于本例子偏移为0),然后赋值。可以判断对于子类CRect来说, 也是以偏移为0的地址进行赋值的;换句话说,子类对象的内存有一块内存是父类的,而且父类的内存必须在内存块的前半部分,要不对rect1的地址偏移为0的地址赋值时,就有可能赋值到另一个数据成员上,而不是m_color。
2,如何验证此想法?
1)根据上面例子的内存打印可以看出,rect1的m_color的内存确实在地址的前半部分。
2)可以给SetColor传递一个假的CShape类型,观测其是否确实是对假的对象的前四个字节赋值?
测试代码如下:
struct FakeCRect{
int fake1;
int fake2;
int fake3;
int fake4;
}fakerect = {1,1,1,1};
TRACE_FUCTION_AND_LINE("fakerect:%d, %d, %d, %d", fakerect.fake1, fakerect.fake2, fakerect.fake3, fakerect.fake4);
CRect* pfakerect = (CRect*)&fakerect;
pfakerect->SetColor(20);
TRACE_FUCTION_AND_LINE("fakerect:%d, %d, %d, %d", fakerect.fake1, fakerect.fake2, fakerect.fake3, fakerect.fake4);
打印结果如下:
main: 23] fakerect:1, 1, 1, 1
main: 26] fakerect:20, 1, 1, 1
可以看到确实是第一个字节变了,传入一个假的CRect类型,它依然是对其第一个int变量处理,你甚至可以传递一个char型的数组来测试都行。
3,拓展延伸,这种情况的例外。
1)上面的例子没有考虑带有虚函数的情况,现在给父类和子类分别加入一个虚函数,display方法。子类继承父类的虚函数,并重写此方法。
virtual void display(){
TRACE_FUCTION_AND_LINE("");//打印当前函数的名字和行号,便于判断是调用哪一个类的display方法。
}
这个时候可以看到,父类和子类的对象内存大小改变了,分别增加了四个字节,分别是8, 16,而且根据子类的PrintMemory可以清楚的看到添加的内存是占用的对象最前面四个字节,其他不变。虚函数机制使得使用基类的指针指向不同的对象来实现多态成为现实。pShape->display();
通过pShape指向不同的对象, 将会调用不同方法,可以再创建一个CCircle类继承于CShape类来观测这种结果。
CShape* pShape = new CCircle();
pShape->display();//调用CCircle的display方法
pShape = new CRect;
pShape->display();//调用CShape的display方法
如果你还不是很清楚虚函数的用法或者说你不打算深入探究虚函数的实现原理,建议一下内容就不要看了。
只要一个类有虚函数(继承下来的或者是本身的),它就会有一个vptr,vptr是一个指针,执行一个vbtl虚函数表。你可以把vbtl想象成一个指针数组,它的数组的元素是一个个的函数指针,指向它自己的虚函数,一般还会有一个指向typeinfo的结构的指针,以实现运行时刻类型识别。
简单说来,现在CRect有一个虚函数display,那么他的vbtl会有两个元素,一个是typeinfo指针,一个是dispaly方法指针。
2)简单看看typeinfo:
CShape* pShape = &rect1;
if(typeid(rect1) == typeid(CRect) && typeid(*pShape) == typeid(CRect)){
TRACE_FUCTION_AND_LINE("rect1 is type CRect");
TRACE_FUCTION_AND_LINE("rect1 name:(%s) raw_name:(%s)", typeid(rect1).name(), typeid(rect1).raw_name());
}
通过上述代码可以清楚看到可以在运行时候判断一个对象时什么类型,即使将rect1的地址转换父类指针,依然可以判断出它实际是什么类型。
typeinfo是一个类一个,编译器编译的时候静态静态分配,每一个带有虚函数的类的对象都会有一个指针指向它,同一个类的对象指针应该一致下面开始测试这一猜想。
看有些书上说的是typeinfo的结构指针位于vtbl虚表的第0个item上,但是我在vc++编译调试没能找到,第0个item上是虚函数display的地址。
于是又改在仿linux环境,MINGW下测试,在第-1个item选项上找到了typeinfo的地址,很是欣慰。但是在windows上还是始终找不到,猜测是在-1的item选项上,不过这个选项应该还有其他的东西,我这么推断的主要原因是除了-1和0item这两个位置,它们的前后地址都是0. 只是-1这个item的位置应该被微软又封装了一下,而不是单纯的指向typeinfo结构。
以下为测试代码:
const type_info* ptypeidinfo = &(typeid(rect1));
TRACE_FUCTION_AND_LINE("ptypeidinfo: %p", ptypeidinfo);
int *p = (int*)&rect1;
int *pp = (int*)(p[0]);//vptr
type_info *prectinfo = (type_info*) (*(pp-1));//pp-0: virtual function address
TRACE_FUCTION_AND_LINE("prectinfo: %p", prectinfo);
vs2008输出:
main: 36] ptypeidinfo: 003A9004
main: 40] prectinfo: 003A7748
MINGW输出:
main: 36] ptypeidinfo: 004085a4
main: 40] prectinfo: 004085a4
在vs2008上对pp-2和pp+1都查看了,都是0地址,因此可以猜测typeinfo肯定跟pp-1有关,只是被封装了而已。
3)回到问题 (上面的例子没有考虑带有虚函数的
补充:软件开发 , C++ ,