(五)羽夏看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  

下一篇

  (六)羽夏看C语言——函数

posted @ 2021-09-05 20:41  寂静的羽夏  阅读(1166)  评论(4编辑  收藏  举报