c++ 对象内存分配和虚函数
1. c++类对象(不含虚函数)在内存中的分布
c++类中有四种成员:静态数据、非静态数据、静态函数、非静态函数。
1. 非静态数据成员放在每个对象内部,作为对象专有的数据成员
2. 静态数据成员被抽取出来放在程序的静态数据区内,为该类所有对象共享,只保留一份
3. 非静态成员函数和静态成员函数最终都被提取出来放在程序的代码段中并为该类所有对象共享,因此每个成员函数也只能存在一份代码实体。
因此,构成对象的只有数据,任何成员函数都不属于任何一个对象,非静态成员函数和对象的关系就像是绑定,绑定的中介就是this指针。
1. 不含有虚函数的对象在内存中分配
如下代码示例:
class Test3{
public:
void f(){
cout << "f in Test3" << endl;
}
static int a;
};
class Test4{
public:
void f(){
cout << "f in Test4" << endl;
}
int a;
}; class Test5{
public:
void f(){
cout << "f in Test5" << endl;
}
int a;
char c;
};
struct Test6{
char c[];
};
int main(){
Test3 test3;
cout << "size of Test3 = " << sizeof(Test3) << endl; //1, 不含有非静态数据
cout << "size of test3 = " << sizeof(test3) << endl; //1 Test4 test4;
cout << "size of Test4 = " << sizeof(Test4) << endl; //4 含有非静态数据int
cout << "size of test4 = " << sizeof(test4) << endl; //4 cout << "size of Test5 = " << sizeof(Test5) << endl; //8,含有非静态数据 int和char
//这里为8,显然是int 和 char对齐之后的结果。 cout << "size of Test6 = " << sizeof(Test6) << endl; //1,结构体或类中,空数组,大小为1
return 0;
}
从以上的代码示例中可以看出,类的对象(不含有虚函数)的size值等于对象中所有非静态成员对齐后的size之和。对齐规则同结构体,详见 结构体对齐.
同时也有,若结构体或类中不含有任何数据,sizeof结果为1;若结构体或类中只有空数组,不占用空间,sizeof结果仍为1;若结构体或类含有数据,最后添加了空数组,空数组不占空间,sizeof为去掉空数组之后的对齐后大小.
那么,如果知道了对象在内存中的地址,就可以得到各个非静态成员的地址,然后可以强行修改,如:
Test3 test4;
int* p = (int*)&test4;
*p = 200; //此时test4对象内部的a值就被修改了
但这种方式很不安全,强烈不推荐!
2. 虚函数
c++中通过虚函数来实现运行时多态,主要方法如下:
编译器对每个包含虚函数的类创建一个表(称为 VTABLE,虚函数表),在VTABLE中,编译器放置特定类的虚函数地址。在每个带有虚函数的类对象中,编译器秘密的内置一个指针,称为 vpointer(VPTR),指向这个对象的VTABLE。通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态的插入取得这个VPTR,并在VTABLE表中查找函数地址的代码,这样就能够调用正确的函数使晚捆绑发生。
为每个类设置VTABLE,初始化VPTR,为虚函数插入调用代码,所有这些都是自动发生的。利用虚函数,这个对象的合适的函数就能被调用,哪怕在编译器还不知道这个对象的特定类型的情况下。(《c++编程思想》)
3. 虚函数表在内存中分布
对于有虚函数的基类和子类来说,内存中类对象的开始位置就是4字节的VPTR(虚函数表的地址,指向VTABLE)。如果一个子类继承自基类,且没有对虚函数进行重写,子类仍然会自己维护一个虚函数表,和基类的虚函数表地址不同(尽管可能内容相同)。
虚函数表中存放该类对应的实际会调用的函数地址(即若子类没有覆盖基类的虚函数,则对应位置存放基类虚函数地址;若子类覆盖了基类的虚函数,则对应位置存放子类的虚函数地址)。虚函数表以NULL结尾。且如果子类中添加了基类中没有的虚函数,则新加的虚函数被放在子类虚函数表最后。
虚函数表只存放虚函数的地址,非虚函数由编译器在编译期间静态设定。
如果一个子类是多继承,即含有多个基类,则有多个虚函数表,分别对应到不同的继承。
可以通过对象的地址,得到虚函数表,单独调用类中的函数:
class A{
public:
A(){
}
~A(){
}
virtual void f1(){
cout << "f1 in class A" << endl;
}
};
class B{
public:
virtual void f2(){
cout << "f2 in clas B" << endl;
}
};
class D: public A, public B{
public:
D(){
}
~D(){
}
void f1(){
cout << "f1 in class D" << endl;
}
void f2(){
cout << "f2 in class D" << endl;
}
};
typedef void(*Func)();
int main(){
D* d = new D();
uint32_t* vtable_ptr = (uint32_t*)*(uint32_t*)d;//d为对象地址,*d即可获得VPTR值
Func f = (Func)(*vtable_ptr);//*VPTR即为第一个虚函数的地址
cout << "func's address is : " << *(uint32_t*)f << endl;
f(); //调用,输出 "f1 in class D"
return 0;
}
4. 单继承的对象在内存中的分配
单继承的对象,如果含有虚函数,则对象在内存中的布局为:开头为VPTR,然后为该对象的非静态成员变量,且为先基类的变量后子类的变量。且如果父类的非静态成员变量为private,子类中仍然会存在那些private的非静态数据。
class Test4{
public:
virtual void f(){
cout << "f in Test4" << endl;
}
int get_a(){
return a;
}
int a;
char c;
};
class Test4{
public:
virtual void f4(){
cout << "f4 in Test4" << endl;
}
int get_a(){
return a;
}
private:
int a;
char c;
};
int main(){
int* pa = (int*)((char*)(&test4) + 4); //+4,是为了跳过VPTR
cout << "first member in test4 = " << *pa << endl; //输出一个随机数
*pa = 200;
cout << "first member in test4 = " << test4.get_a() << endl; //输出200 Test7 test7;
cout << "sizeof test7 = " << sizeof(test7) << endl;
//12字节,4字节的VPTR, 4字节的int a, 4字节(对齐后的)char c。
pa = (int*)((char*)(&test7) + 4);
cout << "first member in test7 = " << *pa << endl;
*pa = 1000;
cout << "first member in test7 = " << test7.get_a() << endl; //输出1000
return 0;
}
结构图大致如下:
5. 多继承的对象在内存中的分配
如果一个子类有多个基类,为多继承。其对象在内存中的布局为:按照多个继承,分为多个块;每块的开头为一种继承的虚函数表,接着为该种继承的基类的非静态成员变量(包括public,protected,private);所有块结束之后,为该子类特有的非静态成员变量;如果该子类又新加了虚函数,则虚函数放在第一个虚函数表的最后。
class A{
public:
virtual void f1(){
cout << "f1 in class A" << endl;
}
private:
int a;
};
class B{
public:
virtual void f2(){
cout << "f2 in class B" << endl;
}
virtual void f4(){
cout << "f4 in class B" << endl;
}
int b;
}
class D:public A, B{
public:
void f1(){
cout << "f1 in class D" << endl;
}
void f2(){
cout << "f2 in class D" << endl;
}
virtual void f3(){
cout << "f3 in class D" << endl;
}
private:
double c;
}
以上类层次结构所对应的对象在内存中的布局为:
6. 虚基类和虚继承
如果有多个类继承自同一个基类,最后又被同一个子类继承这些类(例如 B, C继承自A, D继承自B,C),这种情况下,如果不使用虚继承,若类型A存在非静态成员变量a,则会有D类型的对象保留从B继承来的a和从C继承来的a,出现空间冗余。
而若使用虚继承,可以使得D从B继承来的a和从C继承来的a是同一个,节省空间。
没有虚继承的代码和内存布局
class A{
public:
virtual void f1(){
cout << "f1 in class A" << endl;
}
virtual void f2(){
cout << "f2 in class A" << endl;
}
private:
int a;
};
class B:public A{
public:
void f1(){
cout << "f1 in class B" << endl;
}
virtual void f4(){
cout << "f4 in class B" << endl;
}
};
class C:public A{
public:
void f2(){
cout << "f2 i class C" << endl;
}
virtual void f3(){
cout << "f3 in class C" << endl;
}
};
class D :public B, C{
public:
void f3(){
cout << "f3 in class D" << endl;
}
int d;
}; typedef void(*Func)();
int main(){ A obj_a;
cout << "sizeof obj_a = " << sizeof(obj_a) << endl; B obj_b;
cout << "sizeof obj_b = " << sizeof(obj_b) << endl; C obj_c;
cout << "sizeof obj_c = " << sizeof(obj_c) << endl; D obj_d;
cout << "sizeof obj_d = " << sizeof(obj_d) << endl; uint32_t* vptr_b = reinterpret_cast<uint32_t*>(*(uint32_t*)(&obj_d)); //指向vftable_b Func b_f1 = (Func)(reinterpret_cast<void*>(*vptr_b));
b_f1();
Func a_f2 = (Func)(reinterpret_cast<void*>(*(vptr_b + 1)));
a_f2();
Func b_f4 = (Func)(reinterpret_cast<void*>(*(vptr_b + 2)));
b_f4(); uint32_t* vptr_c = reinterpret_cast<uint32_t*>(*(uint32_t*)((char*)&obj_d + 8));
Func a_f1 = (Func)(reinterpret_cast<void*>(*vptr_c));
a_f1();
Func c_f2 = (Func)(reinterpret_cast<void*>(*(vptr_c + 1)));
c_f2();
Func d_f3 = (Func)(reinterpret_cast<void*>(*(vptr_c + 2)));
d_f3();
return 0;
}
运行结果
内存布局
使用虚继承的代码和内存布局
使用虚继承之后,公共的基类在子类中只有一份,在多重继承的基础上多了vbtable来存储到公共基类的偏移。
class A{
public:
virtual void f1(){
cout << "f1 in class A" << endl;
}
virtual void f2(){
cout << "f2 in class A" << endl;
}
int get_a(){
return a;
}
private:
int a;
};
class B:virtual public A{
public:
void f1(){
cout << "f1 in class B" << endl;
}
virtual void f4(){
cout << "f4 in class B" << endl;
}
};
class C:virtual public A{
public:
void f2(){
cout << "f2 in class C" << endl;
}
virtual void f3(){
cout << "f3 in class C" << endl;
}
};
class D :public B, public C{
public:
void f3(){
cout << "f3 in class D" << endl;
}
int get_d(){
return d;
}
private:
int d;
}; typedef void(*Func)();
int main(){ A obj_a;
cout << "sizeof obj_a = " << sizeof(obj_a) << endl; B obj_b;
cout << "sizeof obj_b = " << sizeof(obj_b) << endl; C obj_c;
cout << "sizeof obj_c = " << sizeof(obj_c) << endl; D obj_d;
cout << "sizeof obj_d = " << sizeof(obj_d) << endl; cout << "d in obj_d = " << obj_d.get_d() << endl;
int* d_ptr = (int*)((char*)&obj_d + 16);
*d_ptr = 200;
cout << "d in obj_d = " << obj_d.get_d() << endl; cout << "a in obj_d = " << obj_d.get_a() << endl;
int* a_ptr = (int*)((char*)&obj_d + 24);
*a_ptr = 1000;
cout << "a in obj_d = " << obj_d.get_a() << endl; uint32_t* vptr_b = reinterpret_cast<uint32_t*>(*(uint32_t*)(&obj_d)); //指向vftable_b Func b_f4 = (Func)(reinterpret_cast<void*>(*vptr_b));
b_f4(); uint32_t* vptr_c = reinterpret_cast<uint32_t*>(*(uint32_t*)((char*)&obj_d + 8));
Func c_f3 = (Func)(reinterpret_cast<void*>(*(vptr_c)));
c_f3(); uint32_t* vptr_a = reinterpret_cast<uint32_t*>(*(uint32_t*)((char*)&obj_d + 20));
Func a_f1 = (Func)(reinterpret_cast<void*>(*(vptr_a)));
a_f1(); Func a_f2 = (Func)(reinterpret_cast<void*>(*(vptr_a + 1)));
a_f2(); uint32_t* ptr = (uint32_t*)((char*)&obj_d + 4);
cout << "ptr = " << *ptr << endl; ptr = (uint32_t*)((char*)&obj_d + 12);
cout << "ptr = " << *ptr << endl; cout << "vptr.a = " << reinterpret_cast<uint32_t>(vptr_a) << endl; return 0; }
运行结果
内存布局
不同编译器可能不同,在vs2013上测试
其中,继承的公共基类A只保留一份数据,且D中对应到基类B和C的虚函数表(vftable@B和vftable@C)中只保留在B或C中新引入的虚函数,而B和C从A中继承的公共虚函数放在vftable@A中。
vftable@B之后为vbtable@B,指向vftable@A,vftable@C之后为vbtable@C,指向vftable@A.
参考
http://blog.csdn.net/starryheavens/article/details/4549616
http://blog.sina.com.cn/s/blog_60e96a410100lirk.html
c++ 对象内存分配和虚函数的更多相关文章
- C++解析(25):关于动态内存分配、虚函数和继承中强制类型转换的疑问
0.目录 1.动态内存分配 1.1 new和malloc的区别 1.2 delete和free的区别 2.虚函数 2.1 构造函数与析构函数是否可以成为虚函数? 2.2 构造函数与析构函数是否可以发生 ...
- C++学习011-常用内存分配及释放函数
C++用有多种方法来分配及释放内存,下面是一些经常使用的内存分配及释放函数 现在我还是一个技术小白,一般用到也指示 new+delete 和 malloc和free 其他的也是在学习中看到,下面的文字 ...
- c++内存分布之虚函数(单一继承)
系列 c++内存分布之虚函数(单一继承) [本文] c++内存分布之虚函数(多继承) 结论 1.虚函数表指针 和 虚函数表 1.1 影响虚函数表指针个数的因素只和派生类的父类个数有关.多一个父类,派生 ...
- [Java]Java类和对象内存分配详解
描述 代码说明: 一.当Person p1 = new Person();第一次被调用时需要做两件事: 1.先判断类加载器是否加载过Person类,如果没有则加载到Person类型到方法区 2.在堆中 ...
- c++内存分布之虚函数(多继承)
系列 c++内存分布之虚函数(单一继承) c++内存分布之虚函数(多继承) [本文] 结论 1.虚函数表指针 和 虚函数表 1.1 影响虚函数表指针个数的因素只和派生类的父类个数有关.多一个父类,派生 ...
- C++_类和动态内存分配3-构造函数中使用new的注意事项
如果在构造函数中使用new来初始化对象的指针成员时必须特别小心. 1 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete. 2 new和delete必须相互兼容.new对应于 ...
- jvm对象内存分配
一.jvm简单结构图 1.jvm内存对象分配整体流程: 1.类加载子系统和方法区 类加载子系统负责从文件系统或者网络中加载Class信息,加载的类信息存放于一块称为方法区的内存空间.除了类的信息外, ...
- Java 对象内存分配与回收
JVM内存区域模型: * 程序计数器,内存区域极小,是当前线程的字节码执行行号指示器: * 虚拟机栈.本地方法栈,即平时所说的“栈”,是虚拟机用来执行方法(包括Java.非Java方法)时,使用的临时 ...
- Golang内存分配内置函数之new函数
new函数用来分配内存,主要分配值类型,比如int.float32.struct等,返回的是指针 package main import ( "fmt" ) func main() ...
随机推荐
- OBD芯片应用开发手册 OBD2开发 内部资料分享 汽车电子通讯开发TDA61 TDA66芯片
OBD产品及各种汽车电子相关的开发.往往需要开发者学习各种汽车协议,深入了解全部OBD规范和汽车各性能参数.这往往需要开发者很长的时间学习研究,大大延缓了OBD产品的上市开发进度.为此深圳芯方案电子公 ...
- 检索COM类工厂中CLSID为{000209FF-0000-0000-C000-000000000046}的组件时失败,原因是出现以下错误: 80070005
检索COM类工厂中CLSID为{000209FF-0000-0000-C000-000000000046}的组件时失败,原因是出现以下错误: 80070005 http://blog.csdn.net ...
- zoj Gao The Sequence
Gao The Sequence Time Limit: 2 Seconds Memory Limit: 65536 KB You are given a sequence of integ ...
- 什么是“鸭子类型(duck typing)”?
在计算机编程世界里会接触到一个知识点 —— duck typing,叫“鸭子类型”. 它有一个形象的解释: “当看到一只鸟走起来像鸭子.游泳起来像鸭子.叫起来也像鸭子,那么这只鸟就可以被称为鸭子. ...
- uafxcwd.lib(afxmem.obj) : error LNK2005 解决方法
项目为非MFC,但要用到MFC的库,使用过程中遇到的问题总结一下 uafxcwd.lib(afxmem.obj) : error LNK2005: "void * __cdecl opera ...
- thinkphp model层外挪,以便多个站点可以通用
/ThinkPHP/ThinkPHP.php 增加如下代码 //非原始代码defined('BASE_LOGIC') or define('BASE_LOGIC', THINK_PATH . '.. ...
- 互联网扫描器 ZMap 完全手册
初识 ZMap ZMap被设计用来针对整个IPv4地址空间或其中的大部分实施综合扫描的工具.ZMap是研究者手中的利器,但在运行ZMap时,请注意,您很有 可能正在以每秒140万个包的速度扫描整个IP ...
- 【leetcode❤python】 9. Palindrome Number
#回文数#Method1:将整数转置和原数比较,一样就是回文数:负数不是回文数#这里反转整数时不需要考虑溢出,但不代表如果是C/C++等语言也不需要考虑class Solution(object): ...
- JAVA开发--U盘EXE恢复工具
原理比较简单,在学校机房U盘总被感染,写一个工具来方便用 package com.udiskrecover; import java.awt.Container; import java.awt.Fl ...
- Java开发中经典的小实例-(随机数)
import java.util.Random;//输出小于33的7个不相同的随机数public class probability { static Random random = new R ...