(五)羽夏看C语言——结构体与类(C++)
写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 (一)羽夏看C语言——简述 ,方便学习本教程。
C/C++结构体❓和类❗
C
没有类这个东西,C++
有。C
有结构体但只能包含数据,不能有函数,但C++
可以。如果读者只想了解C
的汇编底层的话,就不要继续了,以免浪费时间。
类与结构体的关系
它们两个的定义我就不在啰嗦了。在C++
中,类和结构体是一个东西,只是用的关键字不一样罢了。不信咱们做一个实验,看看编译会不会报错:
#include <iostream>
struct MyStruct
{
public:
MyStruct();
~MyStruct();
private:
};
class MyClass
{
public:
MyClass();
~MyClass();
private:
};
MyClass::MyClass()
{
}
MyClass::~MyClass()
{
}
MyStruct::MyStruct()
{
}
MyStruct::~MyStruct()
{
}
int main()
{
system("pause");
return 0;
}
结果编译顺利通过。如果还想继续做深入的实验,请自行研究。下面我们来介绍它们的本质。
汇编看类和结构体
类和结构体虽然没有任何区别,但 通常会把只有数据的称之为结构体,还有功能函数的称之为类 。这句话我曾在(二)羽夏看C语言——容器 说明过。在此文章,我一般将用class
关键字称之为类,用struct
关键字称之为结构体,但脑子里面一定要清楚,C++
中的结构体和类是一个东西。我们将从一下方面对类和结构体进行探讨:
类的实例化
我们将用以下代码进行探讨此问题:
#include <iostream>
class MyClass
{
public:
MyClass();
~MyClass();
int pa = 5;
private:
int a;
};
MyClass::MyClass()
{
a = 10;
}
MyClass::~MyClass()
{
}
int main()
{
MyClass cls;
system("pause");
return 0;
}
以下是反汇编结果,让我们逐个分析类被实例化的过程:
如上图所示,lea ecx,[ebp-10h]
就是取该类的指针,即为this
。这就是为什么编译器在写类可以用this
的原因。下一个call
调用即为调用该类的构造函数。
上面的图是call
调用后到的第一个代码块,可以说明,当一个类实例化时,会先调用它的构造函数。
根据汇编可知,调用构造函数的时候,先初始化变量,然后继续调用构造函数里面的内容,继而完成整个类的实例化。
类中有静态变量或函数
我们将用以下代码进行实验:
#include <iostream>
using namespace std;
class MyClass
{
public:
int pa = 5;
//static int b;
//void test();
private:
int a = 6;
};
//int MyClass::b = 10;
//void MyClass::test()
//{
// cout << "test" << endl;
//}
int main()
{
MyClass cls;
cout << sizeof(MyClass) << endl;
//int tmp = cls.b;
//cls.test();
system("pause");
return 0;
}
一看就能明白,以上代码使用来查看类大小的,我们可以用这种方式来判断这个东西真正属于不属于类。运行后,结果如下:
8
请按任意键继续. . .
然后,我们把b
的声明和初始化以及调用去掉注释,然后再运行一下,发现结果仍和上面的结果一样。我们再看一下它的反汇编,跟到类实例化函数体内:
咦,咋找不到和b相关的任何东西呢,主函数也是没有,在那个b
初始化处下断点也下不住。那我们再看看局部变量窗体里看看有没有与b
有关的讯息:
遗憾的是,调试器里面的局部变量也不承认有b
这个东西。那好,我们唯一能做的是再看一下如何访问这个b
的。
我们发现,b被翻译成一个死地址,说明在类里面声明一个静态变量和在类外面声明一个静态变量在汇编层面没有任何区别,只是在C语言层面不同而已。
接下来看一下函数,我们重新把函数取消注释。继续做实验,发现结果还是相同。然后我们看一下反汇编:
可以看到,函数同样被翻译成一个死地址,但在它之前还是将该类的this指针
传递给函数。如果将函数前面用static
修饰的话,看看反汇编会有什么变化。
可以看到,函数直接被翻译成一个死地址,但不会传递this指针
,这和在类外面声明一个函数调用在汇编层面无异。
继承
在类里面十分重要的一个概念就是继承。那么继承在汇编层面到底是什么样子呢?我们用以下代码进行验证:
#include <iostream>
using namespace std;
class MyClass
{
public:
int pa = 5;
MyClass()
{
cout << "MyClass构造函数被调用" << endl;;
}
private:
int a = 6;
};
class MyClassSub :public MyClass
{
public:
int pb = 15;
MyClassSub()
{
cout << "MyClassSub构造函数被调用" << endl;;
}
private:
int b = 16;
};
int main()
{
MyClassSub cls;
//int a = cls.pb;
//a = cls.pa;
system("pause");
return 0;
}
如下是输出结果:
MyClass构造函数被调用
MyClassSub构造函数被调用
请按任意键继续. . .
这个是我们从C语言层面对构造函数调用顺序进行验证,然后我们看一下反汇编:
根据反汇编,我们也同样验证此问题。然后我们再看一下变量会有什么变化,先把被注释掉的恢复进行验证,把代码运行到构造函数刚好结束,然后在内存窗口输入类的地址,可以得到如下结果:
由此可以看出,类的继承是直接是把被继承的类后面贴上子类的。那么,如果子类有的变量父类也有呢?我们把int pb = 15
改为int pa = 15
,连同下面的代码改动,我们看一下结果。
可以看出,访问pa
的时候直接访问子类
的,而内存结构根本没有发生任何变化。
我们最后再验证最后一个问题:子类继承默认访问为私有的,如果我们把public
删掉后会不会应该继承后的内存结构呢?下一篇将揭晓答案。
虚表
我们从汇编层面观察虚表是什么,将用下面的汇编代码进行实验:
#include <iostream>
using namespace std;
class MyClass
{
public:
int pa = 5;
MyClass()
{
cout << "MyClass构造函数被调用" << endl;;
}
virtual void test();
private:
int a = 6;
};
void MyClass::test()
{
cout << "test" << endl;
}
class MyClassSub :MyClass
{
public:
int pa = 15;
MyClassSub()
{
cout << "MyClassSub构造函数被调用" << endl;;
}
void test();
private:
int b = 16;
};
void MyClassSub::test()
{
cout << "override test" << endl;
}
int main()
{
//请用指针实例化类,如果在堆栈实例化将会调用它的死地址
MyClassSub* cls = new MyClassSub();
cls->test();
system("pause");
return 0;
}
将会得到如下结果:
MyClass构造函数被调用
MyClassSub构造函数被调用
override test
请按任意键继续. . .
在system("pause");
这行下断点,然后运行。观察局部变量,看到如下图:
__vfptr
就是虚表地址,有几个虚函数就有几个。如果被重写,将会将虚表填充对应的地址。我们看看是如何调用该函数的。
通过汇编得出:通过虚表调用子类的test
函数。
拷贝构造函数
在C语言中,每个类都会自带一个拷贝构造函数,我们看看拷贝构造函数为我们做了什么,将用以下代码进行实验:
#include <iostream>
using namespace std;
class MyClass
{
public:
int pa = 5;
MyClass()
{
cout << "MyClass构造函数被调用" << endl;;
}
private:
int a = 6;
};
int main()
{
MyClass cls;
MyClass* ci = new MyClass(cls);
system("pause");
return 0;
}
然后在合适的地方下个断点,看反汇编:
从图中可以看到new
和拷贝构造的过程,先调用new函数
申请8个字节
的内存给类用,然后判断有没有成功,成功后把每个字节对应复制到指定位置。
补充
对于结构体与类,如果里面没有任何初始化,又没有写构造函数,编译器就不会帮我们来写和调用,如下所示:
#include <iostream>
using namespace std;
class MyClass
{
public:
int a;
int b;
int c;
private:
};
int main()
{
MyClass myclass;
return 0;
}
它的反汇编如下:
int main()
{
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,4Ch
00401026 push ebx
00401027 push esi
00401028 push edi
MyClass myclass;
return 0;
00401029 xor eax,eax
}
0040102B pop edi
0040102C pop esi
0040102D pop ebx
0040102E mov esp,ebp
00401030 pop ebp
00401031 ret
如果对它进行初始化,也不会传递this
指针交给类函数进行初始化,如下所示:
int main()
{
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,4Ch
00401026 push ebx
00401027 push esi
00401028 push edi
MyClass myclass {0,1,2};
00401029 mov dword ptr [myclass],0
00401030 mov dword ptr [ebp-8],1
00401037 mov dword ptr [ebp-4],2
return 0;
0040103E xor eax,eax
}
00401040 pop edi
00401041 pop esi
00401042 pop ebx
00401043 mov esp,ebp
00401045 pop ebp
00401046 ret
下一篇
本文来自博客园,作者:寂静的羽夏 ,一个热爱计算机技术的菜鸟
转载请注明原文链接:https://www.cnblogs.com/wingsummer/p/15222668.html