C++对象模型之数据语义学

前言

本次笔记记录如下知识点

  • 数据成员绑定时机
  • 进程内存空间布局
  • 数据成员布局
  • 单一继承下的数据成员布局
  • 虚基类问题的提出和初探
  • 成员变量地址,偏移与指针

一、数据成员绑定时机

成员函数函数体的解析时机

  • 如果成员变量和全局变量重复了,那么成员函数在内部调用时,会优先调用成员变量,因为编译器对函数体内部的变量进行解析时,是从整个类定义完成之后进行的。
    如下示例
string myvar;

class Base
{
public:
	int myfunc()
	{
		return myvar;
	}
private:
	int myvar;
};

int main()
{
	Base a;
	return 0;
}

成员函数参数的确定时机

  • 成员函数的参数类型是在编译器最近一次遇到指定类型来决定,它是本着最近碰到类型的原则来应用最近的碰到的类型
  • 如果将下方示例代码中类的typedef int myvar; myvar m_value;两行代码放到函数下方定义,那么程序就会因为myfunc函数中的赋值代码出现类型不匹配导致程序错误。
typedef string myvar;

class Base
{
private:
	typedef int myvar;
	myvar m_value;
public:
	int myfunc(myvar var)
	{
		m_value = var;
	}
};

int main()
{
	Base a;
	return 0;
}

二、进程内存空间布局

  • 一个可执行文件加载到内存后,就组成了一个进程,进程中的虚拟地址空间有如下几部分

1、栈:局部变量等放在这里。
2、堆:new,malloc等申请的内存空间从这里分配。
3、BSS段:未初始化的全局变量,初始化为0的全局变量等放在这里。
4、数据段:已初始化的全局变量,该数据段包含了BSS段。
5、代码段:存放程序执行代码的一块内存区域。

  • 内存分布如下图所示:
    在这里插入图片描述

三、数据成员布局

边界调整与字节对齐

  • 在类中,每个成员变量的地址都是连续排列的。例如:每个变量都是4个字节,那么变量之间的地址空隙就是4个字节
  • 如果有一些因素导致成员排列不连续,成员的边界会进行调整的,例如:类中所有成员变量都是4个字节,那么突然创建了一个1个字节的变量,此时,边界就会调整这个1字节变量占用的内存,使它成为4或者8的整数倍。
    如下示例
class Base
{
public:
	int a;
	int b;
	char c;
	int d;
};

int main()
{
	Base a;
	a.a = 0;
	a.b = 0;
	a.c = '0';
	a.d = 0;
	cout<<"a地址:" << &a.a <<endl;
	cout<<"b地址:" << &a.b <<endl;
	cout<<"c地址:" << &a.c <<endl;
	cout<<"d地址:" << &a.d <<endl;
	return 0;
}

运行结果
在这里插入图片描述

  • 通过 如上例子我们可以发现,边界为我们对齐了代码中char类型变量的字节。
  • 在某一些情况下,我们不需要这样的对齐方式,那么可以采用一字节对齐,就是针对该类的结构,要求其各个成员之间紧密排列。可以使用如下结构
#pragma pack(1)
//代码结构采用一字节对齐
#pragma pack

四、单一继承下的数据成员布局

在单一继承下,一个子类对象包含的内容,是它自己的成员加上父亲成员的总和。且从成员变量布局来看,父亲成员是先出现的,然后才是子类成员。

通过下面例子分析成员布局

class Base1
{
public:
	int m_i1;
	char m_c1;
};

class Base2 : public Base1 
{
public:
	char m_c2;
};

class Base3 : public Base2
{
public:
	char m_c3;
};

int main()
{
	printf("Base1:%d\n",sizeof(Base1));
	printf("Base2:%d\n",sizeof(Base2));
	printf("Base3:%d\n",sizeof(Base3));
	printf("Base1:m_i1 = %d\n",&Base1::m_i1);
	printf("Base1:m_c1 = %d\n",&Base1::m_c1);
	//这里类的内存空间是以4字节对齐的
	printf("Base2:m_i1 = %d\n",&Base2::m_i1);
	printf("Base2:m_c1 = %d\n",&Base2::m_c1);
	printf("Base2:m_c2 = %d\n",&Base2::m_c2);
	
	return 0;
}

运行结果
在这里插入图片描述

五、虚基类的问题和初探

虚基类的问题

  • 虚基类是具有三层结构,有一个爷爷类,两个父类,一个子类,如下图所示:
    在这里插入图片描述
  • 如上图所示,在孙子类B中,就只会包含一份Grand类的子对象,因为类A1和类A2虚继承Base类。
  • 虚继承的存在就是为了解决多重继承时保证Base类的变量在B类中不会多次出现。
    通过如下例子分析
class Base1
{
public:
	int m_i1;
};

class Base2 :virtual public Base1 
{

};

class Base3 :virtual public Base1
{

};

class B :public Base2,public Base3
{

};

int main()
{
	B b;
	b.m_i1 = 12;
	return 0;
}

虚基类的初探

  • 在虚基类中,父类虚继承爷爷类会在父类中插入一个虚基类表指针,从而影响父类的内存大小。
  • 在孙子类继承父类的时候,也同样会继承父类的虚基类表指针,从而导致孙子类会多出8个字节的内存空间大小。
  • 虚基类与常规的继承不同地方在于对象中的数据布局不同。根据上面的例子画出孙子类B的内存布局。

在这里插入图片描述

六、成员变量地址,偏移与指针

1、可以用一个指针来指向对象的成员变量。
2、成员变量指针里面保存的其实是偏移值,不是真正的内存地址。
3、未经初始化的成员变量指针,编译器给的默认值是0Xcccccccc,当给它赋值成0或者NULL,调试时可以观察到其值是0xFFFFFFFF,但是输出结果的化就是-1。
如下示例


class MYACLS
{
public:
	int m_i;
	int m_j;
	int m_k;
};

int main()
{
	//成员变量指针
	int MYACLS::*mypoint  = &MYACLS::m_i;
	int MYACLS::*mypoint2 = 0;
	printf("mypoint = %d\n",mypoint);
	printf("mypoint2 = %d\n",mypoint2);
	return 0;
}

运行结果如下
在这里插入图片描述


版权声明:本文为qq_40223389原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。