读者若是以为我文章还不错的,但愿能够多多支持下我,文章能够转发,可是必须保留原出处和原做者署名。更多内容请关注个人微信公众号:cpp手艺人。 linux
这个章节咱们主要学习如下几个知识点:ios
1.数据成员绑定时机。git
2.多种模型下数据成员布局。windows
3.数据成员如何读取的。微信
4.进程内存布局函数
你们一看标题可能有点懵了,什么叫数据成员的绑定时机。请随我看段代码,这段代码节选自《深刻探索C++对象模型》工具
extern float x;
class Point3D {
public:
Point3D(float, float, float);
float X() { return x; }
void X(float new_x) { x = new_x; }
private:
float x, y, z;
};
复制代码
若是我调用了Point3D的X()返回的这个x是Point3D的成员变量值,仍是外部定义的x。如今看来的话,很显然返回的Point3D的成员变量值。可是在C++诞生没多久的时代,编译器返回的是外部定义的x。这样的结果对于如今的你来讲是否是有点惊讶。因此你如今看到以下的代码风格,请不要惊讶也不要彷徨。注意下面的高亮部分,将成员变量的定义提早,这样就防护了以前的成员绑定问题。布局
extern float x;
class Point3D {
private:
float x, y, z;
public:
Point3D(float, float, float);
float X() { return x; }
void X(float new_x) { x = new_x; }
};
复制代码
能够看出来现代C++编译器,在绑定成员变量时,是延迟到整个类解析完以后才会进行成员变量的绑定。 可是你们请注意,这里有一个小坑。就是成员函数的参数列表绑定时机。 看以下的代码:学习
#include <string>
using std::string;
typedef string mytype;
class TestDataAnalyse {
public:
int Fun();
// 对于成员函数参数的解析,编译器是第一次遇到这个类型的mytype类型的时候被决定的
// mytype第一次遇到的时候就看到了type string mytype
void CopyValue(mytype value);
private:
int tempvalue;
typedef int mytype;
};
void TestDataAnalyse::CopyValue(mytype value)
{
tempvalue = value;
}
void test_data_analyse() {
TestDataAnalyse data;
data.Fun();
}
复制代码
编译器报错的错误: error C2511: “void TestDataAnalyse::CopyValue(TestDataAnalyse::mytype)”:“TestDataAnalyse”中没有找到重载的成员函数看到这样的错误,你可能找半天都找不到头绪。mytype我已经定义为int类型的,可是为何说没有找到重载的成员函数,这和重载函数有什么关系。测试
缘由就是:成员函数参数的绑定是利用就近原理,当解析到它的时候,它的定义就是最近的定义这个类型的类型。什么意思呢,就是mytype这个类型最近定义这个类型的为string类型(从上往下解析)。CopyValue中将string类型复制给int类型,固然是编译失败。这个时候咱们把类中嵌套的定义typedef int mytype,这句话放在类的开始处,就能够避免这个问题,这个时候mytype的类型就为int。
在这里能够看出成员变量和成员函数参数的绑定时机是不一样的。
class Child {
public:
int m_a;
int m_b;
int m_c;
int m_d;
int m_e;
};
void test_member_layout() {
Child child;
printf("m_a = 0x%p\n", &child.m_a);
printf("m_b = 0x%p\n", &child.m_b);
printf("m_c = 0x%p\n", &child.m_c);
printf("m_d = 0x%p\n", &child.m_d);
printf("m_e = 0x%p\n", &child.m_e);
// m_a = 0x010FFD14
// m_b = 0x010FFD18
// m_c = 0x010FFD1C
// m_d = 0x010FFD20
// m_e = 0x010FFD24
}
复制代码
未继承任何的父类的类,类的成员变量都是按照声明的顺序排列的。根据打印出的地址,以下图所示:
class Base {
public:
int m_base_a;
int m_base_b;
};
class Child : public Base
{
public:
int m_a;
int m_b;
int m_c;
int m_d;
int m_e;
};
void test_member_layout() {
Child child;
printf("m_base_a = 0x%p\n", &child.m_base_a);
printf("m_basse_b = 0x%p\n", &child.m_base_b);
printf("m_a = 0x%p\n", &child.m_a);
printf("m_b = 0x%p\n", &child.m_b);
printf("m_c = 0x%p\n", &child.m_c);
printf("m_d = 0x%p\n", &child.m_d);
printf("m_e = 0x%p\n", &child.m_e);
// m_base_a = 0x00B3FCDC
// m_basse_b = 0x00B3FCE0
// m_a = 0x00B3FCE4
// m_b = 0x00B3FCE8
// m_c = 0x00B3FCEC
// m_d = 0x00B3FCF0
// m_e = 0x00B3FCF4
}
复制代码
单继承无虚函数,先是父类声明的前后顺序,再按照子类的声明的前后顺序,根据打印出的地址,以下图所示:
class Base3 {
public:
int m_base3_a;
int m_base3_b;
};
class Child : public Base, public Base3
{
public:
int m_a;
int m_b;
int m_c;
int m_d;
int m_e;
};
void test_member_layout() {
Child child;
printf("m_base_a = 0x%p\n", &child.m_base_a);
printf("m_basse_b = 0x%p\n", &child.m_base_b);
printf("m_base3_a = 0x%p\n", &child.m_base3_a);
printf("m_basse3_b = 0x%p\n", &child.m_base3_b);
printf("m_a = 0x%p\n", &child.m_a);
printf("m_b = 0x%p\n", &child.m_b);
printf("m_c = 0x%p\n", &child.m_c);
printf("m_d = 0x%p\n", &child.m_d);
printf("m_e = 0x%p\n", &child.m_e);
// m_base_a = 0x010FF6B0
// m_basse_b = 0x010FF6B4
// m_base3_a = 0x010FF6B8
// m_basse3_b = 0x010FF6BC
// m_a = 0x010FF6C0
// m_b = 0x010FF6C4
// m_c = 0x010FF6C8
// m_d = 0x010FF6CC
// m_e = 0x010FF6D0
}
复制代码
当类中出现多个继承父类,成员变量的排列顺序,按照继承的前后顺序排列。若下图所示:
class Base {
public:
int m_base_a;
int m_base_b;
virtual ~Base() {}
};
class Base3 {
public:
int m_base3_a;
int m_base3_b;
// virtual ~Base3() {}
};
class Child : public Base, public Base3
{
public:
int m_a;
int m_b;
int m_c;
int m_d;
int m_e;
};
int main() {
return 0;
}
复制代码
将上面的代码保存为member_layout.cpp,咱们利用在第四节文章里面的用到的工具,在windows菜单找到vs2013 开发人员命令提示工具,双击进入命令行的界面。
运行cl /d1 reportSingleClassLayoutChild member_layout.cpp,在不改变继承的顺序下,手动的添加和删除虚析构函数,导出的结果以下:
从表对比能够看出,继承的父类有没有虚函数是会影响子类的成员的布局。
1.子类继承了多个父类,其中有多个父类有虚函数,会优先排列有虚函数的父类而且按照继承的前后顺序排列,其次再排序无虚函数的父类。
2.若是继承的父类都是有虚函数或者是都没有虚函数,那么都按照继承的前后顺序排列。
将以下代码保存为member_layout.cpp,注意在这里为了探究内存布局,我删除了父类的全部虚函数,这样的设计不合理的,你们请注意。
class Grand {
public:
int G1;
int G11;
};
class Parent1 : virtual public Grand
{
public:
int P1;
};
class Parent2: virtual public Grand
{
public:
int P2;
};
class Child3: public Parent1, public Parent2
{
public:
int C3;
};
int main() {
return 0;
}
复制代码
使用vs2013开发人员命令提示工具,定位你本身的member_layout.cpp目录,好比个人:J:\code\code_git\ \polymorphism_virtual\source,输入命令:cl /d1 reportSingleClassLayoutChild3 member_layout.cpp,Child3表示你要导出的类布局。
我截取重要的内容贴出来:
void test_virtual_base_table() {
Child3 c3;
c3.G1 = 0;
c3.G11 = 11;
c3.P1 = 1;
c3.P2 = 2;
c3.C3 = 3;
}
复制代码
咱们反汇编看下:
00C1DF48 push 1
00C1DF4A lea ecx,[c3] ; 虚基类表指针复制给ecx
00C1DF4D call data_semantics::Child3::Child3 (0C11758h)
c3.G1 = 0;
00C1DF52 mov eax,dword ptr [c3] ; 经过基类表指针-->找到虚基类表首地址
c3.G1 = 0;
00C1DF55 mov ecx,dword ptr [eax+4] ; 跳过虚基类表的前4个字节,找到第二项
00C1DF58 mov dword ptr c3[ecx],0 ; c3+[ecx]偏移,复制给G1所在内存地址为0
c3.G11 = 11;
00C1DF60 mov eax,dword ptr [c3] ; 经过基类表指针-->找到虚基类表首地址
00C1DF63 mov ecx,dword ptr [eax+4] ; 跳过虚基类表的前4个字节,找到第二项
00C1DF66 mov dword ptr [ebp+ecx-20h],0Bh ; 复制给G1所在内存地址为0BH,ebp-20h其实为P1的地址
c3.P1 = 1;
00C1DF6E mov dword ptr [ebp-20h],1
c3.P2 = 2;
00C1DF75 mov dword ptr [ebp-18h],2
c3.C3 = 3;
00C1DF7C mov dword ptr [ebp-14h],3
复制代码
若是你阅读过《第四章 虚函数的原理》,你会发现这个对虚函数的寻址机制是彻底相同的。 都是根据指针->虚表->虚表中的内容,看下图的成员变量的偏移量值。
一样的道理G11也是按照这种原理寻址的。须要注意下mov dword ptr [ebp+ecx-20h],ebp-20h其实P1的首地址,因此从P1的地址开始(跳过前面的vbptr 4个字节)+20正好就是G11的地址。
Child3中的Parent1,Parent2中都包含一个vbptr。这和vptr是十分相似的原理,懂了虚函数的原理,那么你搞懂虚基类表也是很是容易的。
在2.4中为了探究虚继承的内存布局,我把Grand类中的虚析构函数删除。这样的设计是不合理的,这里咱们给Grand加上虚析构函数。关于为何父类中必定要虚析构函数能够参考《第四章 虚函数原理》。
class Grand {
public:
int G1;
int G11;
virtual ~Grand() {}
};
复制代码
咱们再使用工具导出类布局
能够看出Grand中增长一个虚函数,在子类中Child3中的布局中增长了一个vfptr指针,这个指针指向一个表,表中就存储了虚函数的地址,其实就是咱们说的虚函数表,vfptr就是至关于咱们以前讨论的vptr。
还剩下的几种状况虚继承+虚函数的情形,都是相同的分析机制。这里我就再也不分析了。
前面咱们详细的分析了各类模式下,类中内存中的布局。既然咱们知道了数据在哪里,那么接下来讨论数据是怎么读取的。
其实,咱们已经知道数据在内存的位置,将会很是有利于咱们理解对于成员变量的读取,甚至能够说咱们已经理解了一半。
static成员是属于整个类的,并不单独属于某一个实例化的对象。咱们接着借用上面的代码Child3类,在里面增长几行代码。
class Child3: public Parent1, public Parent2
{
public:
int C3;
static int s_Age;
};
int Child3::s_Age = -1;
void test_call_member() {
Child3::s_Age = 0;
Child3 c3;
c3.s_Age = 1;
Child3 *pc3 = new Child3();
pc3->s_Age = 2;
}
复制代码
接下来咱们看下test_call_member反汇编的调用方式有什么不一样。
Child3::s_Age = 0;
0092ADC0 mov dword ptr ds:[93B000h],0
Child3 c3;
0092ADCA push 1
0092ADCC lea ecx,[c3]
0092ADCF call data_semantics::Child3::Child3 (09217A8h)
0092ADD4 mov dword ptr [ebp-4],0
c3.s_Age = 1;
0092ADDB mov dword ptr ds:[93B000h],1
Child3 *pc3 = new Child3();
0092AE26 mov dword ptr ds:[93B000h],2
复制代码
从上述代码中咱们看出,若是是静态成员,无论你的调用方式如何。其实他们最后的汇编代码都是同样的。从侧面能够看出静态数据是放在全局的数据区,和对象是没有关系的。
前面咱们学习了static成员的是怎么找到,可是和实例对象相关的成员咱们该怎么定位呢。
咱们看下面的代码:
class Child4 {
public:
int m1;
int m2;
int m3;
int m4;
};
void test_member_initialize() {
Child4 *c5 = new Child4();
c5->m1 = 0;ss
c5->m2 = 1;
c5->m3 = 2;
c5->m4 = 3;
Child4 c4;
c4.m1 = 0;
c4.m2 = 1;
c4.m3 = 2;
c4.m4 = 3;
}
复制代码
接下来咱们经过反汇编代码看下是如何定位成员变量。
; 第一段
c5->m1 = 0;
011AC94F mov eax,dword ptr [c5]
011AC952 mov dword ptr [eax],0
c5->m2 = 1;
011AC958 mov eax,dword ptr [c5]
011AC95B mov dword ptr [eax+4],1
c5->m3 = 2;
011AC962 mov eax,dword ptr [c5]
011AC965 mov dword ptr [eax+8],2
c5->m4 = 3;
011AC96C mov eax,dword ptr [c5]
011AC96F mov dword ptr [eax+0Ch],3
; 第二段
Child4 c4;
c4.m1 = 0;
011AC976 mov dword ptr [c4],0
c4.m2 = 1;
011AC97D mov dword ptr [ebp-1Ch],1
c4.m3 = 2;
011AC984 mov dword ptr [ebp-18h],2
c4.m4 = 3;
011AC98B mov dword ptr [ebp-14h],3
复制代码
咱们先看第一段汇编代码,首先[c5]中保存对象的首地址给eax,对eax解引用而后赋值0。再对(eax+4)解引用赋值1,再对(eax+8)解引用赋值2,再对(eax+12)解引用赋值3。我就能够看出这里根据对象的首地址+成员变量的偏移,就能找到对应的对象而后进行读取操做。
这时咱们再看第二段的汇编代码,你们可能就奇怪了,为何第二种的定位方式第一种不一样呢。
其实这里定位的原理是一致的。第一种方式,c5对象是new出来,也就是说它的内存是堆中的,堆区是个很是灵活的区域申请和释放都是本身能够控制的,同时它的增加的方向是从低地址->高地址。而第二种方式呢,c4是在栈中申请的内存,相比较堆呢,它的可控性就弱了不少,并且它的增加方向是从高地址->低地址,这一点和堆是彻底相反的,记住ebp表示的基址指针寄存器,该指针指向系统栈最上面一个栈帧的底部。
咱们作个表格对比下两种:
采用的原理都是同样的,都是利用成员变量的偏移量,只不过它们的增加的方式不同。堆往上增加,栈是往下增加。
class Base6 {
public:
int m_base6_a;
virtual ~Base6() {}
static int s_base6_b;
};
int Base6::s_base6_b = 0;
class Base4 : virtual public Base6
{
public:
int m_base4_a;
int m_base4_b;
static int s_base4_c;
virtual ~Base4() {}
};
int Base4::s_base4_c = 0;
class Base5 : virtual public Base6
{
public:
int m_base5_a;
int m_base5_b;
static int s_base5_c;
virtual ~Base5() {}
};
int Base5::s_base5_c = 0;
class Child5 : public Base4, public Base5
{
public:
int m_a;
int m_b;
int m_c;
int m_d;
int m_e;
};
void test_member_effective() {
Child5::s_base6_b = 10;
Child5::s_base4_c = 1;
Child5::s_base5_c = 2;
Child5 c5;
c5.m_a = 3;
c5.m_base4_a = 4;
c5.m_base5_a = 5;
c5.m_base6_a = 6;
}
复制代码
咱们从汇编的角度(debug 模式下)看下他们的效率。
指向类的成员变量指针,其实不是指针,实际的内容实际上是在整个类中的偏移值。举个例子:
class Class7 {
public:
virtual void VirFunc() {}
int m_a;
int m_b;
int m_c;
int m_d;
};
void test_member_point() {
printf("&Class7::m_a:%x\n", &Class7::m_a);
printf("&Class7::m_b:%x\n", &Class7::m_b);
printf("&Class7::m_c:%x\n", &Class7::m_c);
printf("&Class7::m_d:%x\n", &Class7::m_d);
// &Class7::m_a:4
// &Class7::m_b:8
// &Class7::m_c:c
// &Class7::m_d:10
// 用法
int Class7::*cpoint = &Class7::m_a;
Class7 c7;
c7.*cpoint = 1;
// *(&c7+cpoint) = 1;
}
复制代码
从代码中能够看出来,打印的是成员变量的偏移值。值的注意下就是这里打印的偏移值是从4开始的,由于Class7含有虚函数,存在一个虚表指针。
咱们再看下成员指针的初始化和使用,int Class7::*cpoint = &Class7::m_a; Class7 c7; c7.*cpoint = 1;用法其实很简单,可是困扰个人是这个成员变量指针到底有啥用。我尝试google下也没有发现有价值的资料,从《深刻探索C++对象模型》里面提到了这个能够帮咱们看出成员变量的偏移位置,可是我以为这个意义不大的。这个让我很困惑。
变量在内存是如何分布的,咱们借助linux上nm(names)命令看下,代码以下
#include <stdio.h>
#include <iostream>
using std::cout;
using std::endl;
int g1;
int g2;
int g3 = 3;
int g4 = 4;
int g5;
int g6 = 1;
static int gs_7;
static int gs_8 = 0;
static int gs_9 = 1;
void g_func() {
return;
}
class MyClass {
public:
int m_i;
static int m_si;
int m_j;
// 声明
static int m_sj;
int m_k;
static int m_sk;
};
// 定义
int MyClass::m_sj = 1;
int main(int argc ,char *argv[]) {
int temp_i = 0;
printf("tempi address = %p\n", &temp_i);
printf("g1 address = %p\n", &g1);
printf("g2 address = %p\n", &g2);
printf("g3 address = %p\n", &g3);
printf("g4 address = %p\n", &g4);
printf("g5 address = %p\n", &g5);
printf("g6 address = %p\n", &g6);
printf("gs_7 address = %p\n", &gs_7);
printf("gs_8 address = %p\n", &gs_8);
printf("gs_9 address = %p\n", &gs_9);
printf("MyClass::m_sj address = %p\n", &(MyClass::m_sj));
printf("g_func() address = %p\n", g_func);
printf("main() address = %p\n", main);
cout << "g_test " << (void *)g_test << endl;
return 0;
}
复制代码
1.使用g++ process_member_layout -o process_memeber_layout
2.在使用nm process_memeber_layout导出的数据以下所示。
这里解释下nm中间字段的含义
咱们再对比下nm导出的数据和上述代码的打印的地址
我从网上找了张内存映射图