【C++】类的内部构造原理-实验报告

实验目的

探索C++类的内存结构、访问机制。并且探索编译在32位与64位下的不同。

1 成员变量与成员函数

代码

class B {
public:
   char* zp;
   int len;
};

class classA{
public:
   classA(int m,int n);
   ~classA();
   char c = 'a';
   int publicNum1=1;
   int pubFun1();
   B vec;
private:
   short priNum1=2;
};

classA::classA(int m,int n){
   publicNum1 = m; priNum1 = n;
   vec.zp = new char[4]{1,2,3,4};
   vec.len = 4;
   cout << "creat class A" << endl;
}

classA::~classA(){cout << "delate class A" << endl;}

int classA::pubFun1(){return priNum1+publicNum1;}

int main() {
   classA *p1 = new classA(1, 2);
   classA *p2 = new classA(3, 4);
   cout << "sizeof " << sizeof(*p1) << endl;
   p1->pubFun1();
   p2->pubFun1();
   while (1);
}

成员变量

在x64的环境下,开始编译调试。首先可以看到程序输出为:

classA的大小为32字节

然后用vs可以查看p1的内存地址为0x00000297f45f95e0,找到这块地址。
![](https://img-blog.csdnimg.cn/20200903115028297.png#pic_center =x100)
可以看到其内存结构与结构体一样,一是小端的存储方式,二是同样需要内存对齐
其中

内存地址 变量
E0-E3 c加上补全
E4-E7 publicNum1
E8-EF vec.zp
F0-F7 vec.len 加上补全
F8-FF priNum1加上补全

同理我们可以分析在x86环境下的内存结构


在知道内存存储方式后,我们可以利用这一点,在设计类的时候可以节省空间。

成员函数

我们可以看到内存里成员变量都有,但是没有成员函数指针,我们通过反汇编可以看到
反汇编
p1与p2的成员函数是跳转到同一个地址

![跳转](https://img-blog.csdnimg.cn/20200903124330664.png#pic_center =x40)

所以同样的类,不同的实例,调用的函数体是相同的,只不过传入的参数不同,其中默认的一个传入参数就是当前这个实例的的指针。

2 静态成员

举一个不正确的例子:

class classA {
public:
	int a=1;
	static int sa = 3;
	char c = 'a';
	static int fun(int input) {
		return a + input;
	};
};

这里会报两个错误:
错误
一个是静态变量sa不能在内部被初始化,要不就定义为常量。
二个是静态成员函数中无法调用成员变量a。
带着疑问我们开始分析内存构造与访问机制。

静态成员变量

首先写个正确的例子

class classA {
public:
	int a=1;
	static int sa ;
	int c = 2;
	static int fun(int input) {
		return input;
	};
};
//初始化静态变量,static 成员变量必须在类声明的外部初始化
int classA::sa = 8;

int main() {
	classA *p = new classA;
	cout << "size:" << sizeof(*p) << endl;
	int b = p->fun(23);
	while (1);
}

我们得到的size为8
我们查看p指向的内容:

地址 内容
0x000002705197FF40 01 00 00 00 02 00 00 00
我们好像只看到了变量a和c,没有看到sa。然而我们可以在全局/静态存储区见到他。所以这个变量的属性应该算是全局变量了。无论实例化了多少个对象,他们sa变量地址都是这一个。
然后我们分析成员函数static int fun

静态成员函数

我们可以看到他的反汇编代码:
汇编
这里注意到并没有把p作为参数传入到fun,而只是把23这个入参传进去了。也就是说静态成员函数里是没有this的。所以它识别不了a变量。那么这其实和普通的函数没有什么区别了。

3 虚函数

虚函数最大的功能就是引入多态,在B1、B2继承A的时候,会重写A中的虚函数fun,这时函数体就不一样了,必定会有不一样的函数入口。B1的fun函数与B2的fun函数入口当然不一样。但是在多态的运用中一个对象A可以指向B1也可以指向B2,在编译的时候,我们不会知道A究竟最后是代表的B1还是B2。所以当编译器翻译虚函数A.fun的时候,不能直接翻译成跳转到某个函数地址。 而是在程序执行到这里的时候,需要读取某个内存来判断应该跳转到哪里。

代码

class person {
public:
	int age=20;
	char sex='m';
	int ticket_price = 100;
	virtual int buy(int buyNum) {
		return buyNum * ticket_price;
	};
	virtual void run() {
		cout << "person run" << endl;
	}
};
class student:public person {
public:
	int ID = 0x123456;
	virtual int buy(int buyNum) {
		return buyNum * ticket_price/2;
	};
};
class boss : public person {
public:
	virtual int buy(int buyNum) {
		return 0;
	};
};
int main() {
	vector<string> str{ "person","student","boss" };
	int len = str.size();
	auto p=new person*[len];
	cout << "personsize:" << sizeof(person) << endl;
	cout << "studentsize:" << sizeof(student) << endl;
	//工厂
	for (int i = 0; i < len; i++) {
		if (str[i] == "person") p[i] =  new person;
		else if (str[i] == "student") p[i] =  new student;
		else if (str[i] == "boss") p[i] =  new boss;
		else p[i] =  new person;
	}
	
	for (int i = 0; i < len; i++) {
		cout << p[i]->buy(1) << endl;
	}
}

程序可以用字符串来得到不同的类。这代表着,编译的时候编译器根本不知道p[i]的类型,也就根本没法直接翻译跳转地址。
这里说明一下,多态只能用指针或者应用实现,通过调试程序,我们知道personsize:24、studentsize:32。如果直接用person a=student(),他会给你强制转换类型,但是不会动你的虚函数表。而指针就是直接指向student对象。

回到正题,我们可以查看p[0]指向的内存:
student对象
其中内存结构为:

地址 内容 意义
0x000001E5C7378F20 90 43 39 3c f7 7f 00 00 虚函数表头地址
0x000001E5C7378F28 14 00 00 00 6d cd cd cd 基类中的年龄和性别
0x000001E5C7378F30 64 00 00 00 cd cd cd cd 基类中的票价
0x000001E5C7378F38 56 34 12 00 cd cd cd cd 学生派生类中的ID号
其中顺序就是虚函数表头地址占第一个,然后就是基类中的变量,然后就是派生类中的变量。
而虚函数表中就存了当前对象的虚函数的地址:
虚函数表
可以直接在监视器中查看:
在这里可以清楚的看到虚函数表一般是以0结尾,然后这里虚函数表中有两个指针,第二个指向person::run。这是没有被重写的函数。

我们再看看反汇编:

再调用call之前,编译器会判断调用的虚函数是位于基类中的第几个虚函数,来选择偏移量。得到函数地址,进行跳转。同样也传入了this参数。

结束

下次根据已经掌握的类的结构,来探索类继承的原理。

原文地址:https://www.cnblogs.com/kidtic/p/14219878.html