C++虚函数的一点分析与思考 (一)

2014-11-24 02:27:51 · 作者: · 浏览: 12

简介:
以下是自己看过的书籍以及自己思考的流程和总结,主要是对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
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, 1