实验目的
探索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的环境下,开始编译调试。首先可以看到程序输出为:
然后用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]指向的内存:
其中内存结构为:
地址 | 内容 | 意义 |
---|---|---|
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参数。
结束
下次根据已经掌握的类的结构,来探索类继承的原理。