滴水三期:day15.1-结构体

一、结构体定义

  • 当你需要一个容器能够存储5个数据,这5个数据有1字节的,有2字节的,有10字节的。。你会怎么做?

  • 因为数组中只能存储相同类型的数据,比如都是int型、或char型等。但是现在用一个容器存储不同数据类型的数据,此时就要引出另一个数据类型:结构体

  • 结构体定义:

    struct AA{	      //struct是一个关键字 AA是用户自己定义的一个名字	
        //可以定义多种类型
    	int a; 
    	char b;
    	short c;
        ....
    };
    
  • 此时要用st(你起得结构体的名字)来表示结构体容器,比如你想定义一个上述定义的结构体数据类型变量,那么就要定义为AA x。就跟int xshort x一样

  • 注意:在定义结构体时,编译器还没有给AA分配空间!!仅仅只是告诉编译器,我自定义了一个结构体类型,叫AA

  • 注意:结构体在定义时,不能给当中的变量赋初始值!!要使用下面的赋值方法赋值,先赋值再使用

  • 基址和偏移

    • 基址:一般都是全局变量,因为全局变量在程序编译时就分配了指定的内存空间,所以地址是相对确定的。我们可以通过找到基址来得到其他数据或者结构的地址。比如上述如果定义了结构体类型的变量AA x,那么x就表示一个基址
    • 偏移:以基址作为出发点,向后数多少个地址。比如上述x是基址,那么x + 4就是AA结构体中第一个变量a;x + 5就是AA中的b变量等
  • 二级偏移:

    struct Point{    //定义一个结构体类型起名为Point,这个Point容器中存储了3个float类型的变量
    	float x;
        float y;
        float z;  //坐标
    };
    
    struct AA{
        int life;  //生命
        int magic; //魔法
        int skill; //技能
        Point Der; //坐标     
        
        float speed; //移动速度
        char name[20];  //20字符,10中文   
    };
    
    • 此时AA的地址为基址,用AA加偏移量得到结构中的变量内存存储地址,比如:

      • AA + 4 ----生命

      • AA + 8 ----魔法

      • AA + 0xC ----技能

      • AA + 0x10 ----坐标

        因为AA + 0x10 得到的又是一个基地址,Point结构体,所以此时可以根据Point的基址再偏移得到Point结构体中的变量

        • Der + 0x4 ----x坐标
        • Der + 0x8 ----y坐标
        • Der + 0xC ----z坐标
  • 综上:结构体在定义的时候,除了自身以外,可以使用任何类型,包括结果体类型

    比如不能在结构体AA中定义AA类型的变量

二、结构体赋值和使用

  • 注意结构体定义和使用时是否分配内存以及何时分配内存!(小提示:注意定义的位置是全局变量还是局部变量)

    struct st1{	   //此时只是告诉编译器我定义了一个结构体类型,叫st1,此时不给st1和其中的数据分配内存
    	int a;
    	int b;
    };	
    struct st2{	   //此时不分配内存空间
    	char a;
    	short b;
    	int arr[10];
    	st1 s;
    };	
    
    int m = 2;   //全局变量在编译后就分配了内存空间
    st2 sss;  //这是定义了一个st2类型的变量sss,但是是全局变量,所以在编译时就给st2类型的sss分配了固定的内				存空间,当中成员默认初始值为0
    
    void Funtion(){		//只有当Function函数被调用时,才会给当中的局部变量分配内存空间!
    	st2 s2;     //调用后就给st2结构体类型的局部变量s2分配了内存,至于分配多少内存后面学习
        			//注意st1和st2只是结构体类型,但是真正使用时要定义st1或者st2类型的变量,用变量.使用
    	
    	s2.a = 'A';   //给结构体中变量赋值
    	s2.b = 12;
    	s2.arr[0] = 1;
    	s2.arr[1] = 2;
    	s2.arr[3] = 3;
    	s2.s.a = 100;   //给st2结构体中的变量st1结构体中的a变量赋值
    	s2.s.b = 200;
    	
    	printf("%d\n",s2.s.a);  //上面赋值了再读取,不然就是0
    }
    

三、结构体分配内存

  • 先来想一下数组的反汇编指令,最重要的两个特征就是存储时地址是等宽的、连续的
  • 如果对内存的读取或赋值时是连续的但是不等宽的,那么很可能就是结构体

1.作为全局变量分配内存

  • 如果将结构体变量作为全局变量时,如何分配内存:

    #include "stdafx.h"
    struct st2{
    	char a;
    	short b;
    	int arr[10];
    };
    st2 s1;  //全局变量
    void Func(){
        s1.a = 1;    //在这里设置断点
        s1.b = 2.2;
        s1.arr[0] = 3;
        s1.arr[3] = 4;
        printf("%d %d",s1.a,s1.arr[3]);
    }
    void main(int argc,char* argv[]){
    	Func();
    }
    
    • 查看反汇编,发现确实是使用基址加偏移量的方式查找的结构体中的数据,而是可以看到st2类型的变量s1中的数据的地址是固定的,因为结构体类型变量s1是全局变量,在编译时就分配了固定的内存空间。且地址是连续但不等宽的。那么就表示给全局变量赋值,就是直接在给全局变量–st2类型的s1分配的内存中修改数据

      image-20211209100231442

2.作为局部变量分配内存

  • 如果将结构体变量作为局部变量时,如何分配内存:

    #include "stdafx.h"
    struct st2{
    	char a;
    	short b;
    	int arr[10];
    };
    void Func(){
    	st2 s1;   //局部变量
        s1.a = 1;
        s1.b = 2.2;
        s1.arr[0] = 3;
        s1.arr[3] = 4;
        printf("%d %d",s1.a,s1.arr[3]);
    }
    void main(int argc,char* argv[]){
    	Func();
    }
    
    • 查看反汇编,发现s1在被函数调用时才会动态分配内存空间,且是在缓冲区中,地址不是固定的,而是用[ebp - xxx]来查找的。和局部变量的查找方式类似(而且分配空间是按照变量顺序分配的,但是是从低地址向高地址存储的,和数组类似正着从低地址向高地址存)

      image-20211209100750399
    • 但是其实从这里就判断是什么数据类型不准确,或者无法确定,比如当结构体中定义相同类型时,且结构体类型的变量作为局部变量被定义时:会发现和数组分配空间并赋值的方式一模一样,所以无法从这里判断出来

      #include "stdafx.h"
      struct st2{
      	int a;
      	int b;
      	int c;
      };
      void Func(){
      	st2 s1;   //局部变量
          s1.a = 1;
          s1.b = 2;
          s1.c = 3;
      }
      void main(int argc,char* argv[]){
      	Func();
      }
      
      image-20211209102321095
  • 但是要注意结构体类型变量作为局部变量,为不同数据类型分配内存空间大小不再都是4字节,而是有一个结构体对齐的概念,后面会学习。

    • 比如如果定义成多个不同数据类型的普通整数类型的局部变量,那么不管数据类型多宽(32位计算机),都会分配4字节内存存放,因为符合本机尺寸的规则使计算机读写效率更高

      void Func(){
      	char x = 1;
      	int y = 1;
          short z = 1;
      }
      void main(int argc,char* argv[]){
          Func();
      }
      
      屏幕截图 2021-12-09 103850
    • 如果是把结构体类型的变量作为局部变量被定义,结构体当中定义了多个不同类型的变量,那么计算机不会都使用4字节的内存来存储结构体当中的不同数据类型变量。如果结构体中的元素定义的大小顺序不一样,分配的空间不是确定的!

      • 如果按照下方顺序定义:st1类型的局部变量s1中的char、int、short类型变量分配空间方式如下:

        #include "stdafx.h"
        struct st1{  
        	char x;   //如果按照这个顺序定义
        	int y;
            short z;
        };
        void Func(){
        	st1 s1;
        	s1.x = 1;  //在这里设置断点
        	s1.y = 1;
        	s1.z = 1;
        }
        void main(int argc,char* argv[]){
            Func();
        }
        
        屏幕截图 2021-12-09 105630
      • 如果换一种顺序定义:st1类型的局部变量s1中的char、int、short类型变量分配空间方式如下:

        #include "stdafx.h"
        struct st1{
        	int x;    //换了一下数据类型的顺序
        	char y;
            short z;
        };
        void Func(){
        	st1 s1;
        	s1.x = 1;  //在这里设置断点
        	s1.y = 1;
        	s1.z = 1;
        }
        void main(int argc,char* argv[]){
            Func();
        }
        

        image-20211209110026334

      综上可以发现结构体类型变量作为局部变量和普通的局部变量分配内存空间还是有点区别的,结构体具体怎么分配涉及到结构体对齐,明天会详解

四、结构体变量做返回值

  • 因为eax只能存下4字节,而返回值为结构体类型,就是要把结构体当中定义的变量的值返回,eax明显存不下,而且一旦函数执行完后,肯定要把在函数中对结构体中变量的值做的修改想办法返回出来,因为不返回虽然这些值在堆栈中,但是由于函数调用完成要堆栈平衡,那么函数执行时内存中的值都成了垃圾值。所以结构体变量作为返回值是怎么传出来的呢?

    #include "stdafx.h"
    struct st1{
    	char a;
        short b;
        int c;
        int d;
        int e;
    };
    st1 Func(){  //重点关注这里返回值
    	st1 s1;
    	s1.a = 1;
    	s1.b = 2;
    	s1.c = 3;
        s1.d = 4;
        s1.e = 5;
        return s1;
    }
    void main(int argc,char* argv[]){
        st1 s = Func();
    }
    
    • 通过汇编代码可以发现,在call指令前加了两行指令,因为结构体中还定义了很多类型的变量,光用eax和几个寄存器肯定是不够的,那么编译器所做的是,在main函数的缓冲区中选一块起始地址[ebp-30h],然后将这个地址作为返回数据存储的起始地址,从这个地址开始往高位地址依次存下去。但是由于eax寄存器在进入Func函数后要被使用用来存储其他的东西,比如填充Func函数缓冲区的时候要用eax。那么为了不让eax中此时存的返回值起始地址丢了,就将它入栈!像参数一样,然后后面进入Func函数后通过[ebp + 8]来得到这个值

      屏幕截图 2021-12-09 160822
    • 进入函数后可以发现将结构体变量作为返回值,其实就是将在Func函数中给结构体中变量赋的值,赋值一份到main函数的缓冲区中,就是在未进入Func函数是记录的[ebp -30h]这个地址开始往高地址存,最后再把这个返回值存入的起始地址再存入eax中作为返回值,即返回了一个地址

      097026F9258E2AD6C1605D1365AC538D
    • 堆栈图如下图:(不是严格按照宽度画的,主要是体会这个返回值结果的复制的过程)

      image-20211209162246365 屏幕截图 2021-12-09 163105

五、结构体作为参数传入函数过程

结构体变量作为参数传递时,不会像我们前面学的普通变量作为参数一样遵循本机尺寸规则,无论传入多少宽度的数据类型参数,都使用本机尺寸的宽度容器来存储(假设是32位计算机),那么int、char、short类型都使用32位寄存器或者内存来存储(传递)。即如果使用__cdecl调用约定,那么使用push指令来存入到堆栈中(倒着存)

  • 结构体变量作为参数时,会压缩空间,如果一个数据类型的变量宽度不足4字节,而下一个数据类型变量与这个变量加起来的数据宽度用4字节完全可以存下,那么就会合并用一个32位内存来传递两个变量

    #include "stdafx.h"
    struct st1{
    	int x;
    	char y;
        short z;
    };
    void Func(){
    	st1 s1;
    	s1.x = 1;
    	s1.y = 2;
    	s1.z = 3;
    }
    void main(int argc,char* argv[]){
        Func();
    }
    
    • 当main函数调用时,st1 s1;:结构体类型的局部变量s1就被分配了空间,怎么分配的呢?要看明天的结构体对齐,先不深入。按照上述st1中变量定义的顺序,分配如下:[ebp - 8]分配给了x,[ebp - 4]分配给了y,[ebp - 2]分配给了z,没有按照本机尺寸的规则任何数据变量都分4字节。然后依次赋值1,2,3

      image-20211209114916294
    • 接着将结构体变量s1作为参数传入Func函数,可以看到反汇编中没有三个push语句,而是把y和z合并,使用[ebp - 4]表示的4字节内存传递的,所以只有两个push。结构体作为参数传递和普通变量作为参数传递还是有区别的

      屏幕截图 2021-12-09 115132 屏幕截图 2021-12-09 115429

  • 如果当结构体中定义了多个变量,那么结构体作为参数传参就不再一个一个使用push指令了

    #include "stdafx.h"
    struct st1{
    	int x;
    	char y;
        short z;
    	int a;
    	int b;
    	int c;
    	char d;
        short e;
    };
    
    void Func(st1 s){ //重点关注这里,结构体作为参数如何传入函数的
    	
    }
    
    void main(int argc,char* argv[]){
        st1 s1;
        s1.x = 1;
    	s1.y = 2;
    	s1.z = 3;
    	s1.a = 4;
    	s1.b = 5;
    	s1.c = 6;
    	s1.d = 7;
        s1.e = 8;
    	Func(s1);
    }
    
    • 当main函数被调用,st1 s1:就会给s1分配空间,分配并赋值如下,和上面类似没有区别,都是正着从低地址向高地址存储到main函数的缓冲区。且同样遵循结构体对齐的规则,不会任何数据类型变量都分配4字节内存空间。

      屏幕截图 2021-12-09 123848 屏幕截图 2021-12-09 124125

    • 但是如果结构体中定义了大量的变量,而此结构体变量在作为参数出传递时,就不再每一个结构体中的变量都使用push语句一个一个倒着入栈。而是将栈顶一次性提升0x18,因为要将st1结构体中的7个变量入栈,为了效率直接将栈顶提升0x18字节,刚好为存放7个结构体中的变量做准备。然后使用rep循环指令,ecx决定循环次数,重复执行7次movsd指令,[esi]最开始表示[ebp - 18h]就表示为结构体中第一个变量分配的内存地址;[edi]就表示此时提升过后的栈顶;那么第一次movsb指令会第一个变量x的值存入堆栈,然后esi + 4、edi + 4(注意:这里不是因为满足本机尺寸才4个字节4个字节传的,而是movsb指令的规则就是+4,这个过程只是为了将缓冲区中存放有结构体中变量的值的那些内存按照次序复制压入栈顶而已,可能一次复制的4个字节中包含了两个合并使用一个4字节内存的变量值,还是要看最开始结构体赋值时是如何分配的。这里如果满足本机尺寸传参那么就应该提升7 * 0x4 = 0x1C个字节长度,这样才能保证任何类型变量都用4字节内存传递),那么就相当于向下取了一个,[esi + 4]就刚好取到了结构体中第二个变量的内存地址;接着第二次执行movsb指令,将y值入栈;后面依次将st1结构体中的所有变量从低地址到高地址依次入栈,最后一个变量所在内存地址就是原来mian堆栈的栈顶 - 4;这个过程饿和push没什么区别,只是一次性将esp的值提升完了,然后利用循环一个一个存入堆栈

      屏幕截图 2021-12-09 121050 image-20211209125140088

  • 看了上述结构体变量作为参数传入函数的过程会发现,如果一个结构体中定义了大量的数据,可能还会结构体中套结构体,那么每一次将这个结构体变量作为参数传入函数中时,都有一个将结构体中大量变量的值复制一份入栈的过程,然后函数中如果要使用这些参数,都是使用的复制入栈的值,这样非常不合理!效率低,内存开销大!

  • 那么现在没有学指针前我们最好把结构体类型变量定义成全局变量(这样如果在函数中直接定义该结构体类型变量以及赋值的操作,就直接是对编译时为全局变量分配的内存中的数据操作,而不会再重新开辟一个空间[ebp-x]等。但是如果把结构体类型变量作为参数传递,无论是全局还是局部,都有一个复制的过程,如下说明。只有后面学了指针,传地址可以解决这个问题)

    所以在没用到指针前,最好不要把结构体类型变量作为参数或者返回值传递

    • st1结构体变量s作为全局变量,在程序编译时就分配了固定的内存空间,并且结构体中所有变量的值都默认为0。那么采用这种传入参数后再赋值的方式,由于调用Func函数函数前编译器会根据传入的参数是结构体类型的,自动先将st1中的变量全部复制一份入栈供Func函数内部使用,然后函数内部对s中的变量有赋值的操作,所以会将复制过来的值覆盖但是s中真正的变量值没有被影响,即全局变量分配的地址中的存放结构体st1中变量的值未受影响!!!!

      屏幕截图 2021-12-09 150328
      #include "stdafx.h"
      struct st1{
      	int x;
      	char y;
          short z;
      	int a;
      	int b;
      	int c;
      	char d;
          short e;
      };
      st1 s;   //定义成全局变量
      
      void Func(st1 s){
      	s.x = 1;    //传入参数后再赋值
      	s.y = 2;
      	s.z = 3;
      	s.a = 4;
      	s.b = 5;
      	s.c = 6;
      	s.d = 7;
          s.e = 8;
      }
      
      void main(int argc,char* argv[]){
          Func(s);
      }
      
    • 同理如果在Func函数调用之前就在main函数中赋值,就会改变分配给全局变量s内存中存放st1中变量的值,而且之后将s再作为参数传入Func函数,也是先复制一份入栈,然后Func函数中对s的操作都是对复制入栈的值的操作,不会影响全局变量。所以如果想得到赋值后的结果,应该将结构体类型变量返回

      #include "stdafx.h"
      struct st1{
      	int x;
      	char y;
          short z;
      	int a;
      	int b;
      	int c;
      	char d;
          short e;
      };
      st1 s;   //定义成全局变量
      
      void Func(st1 s){
      	//...
      }
      
      void main(int argc,char* argv[]){
      	s.x = 1;   //在Func函数调用之前就在main函数中赋值
      	s.y = 2;
      	s.z = 3;
      	s.a = 4;
      	s.b = 5;
      	s.c = 6;
      	s.d = 7;
          s.e = 8;
          Func(s);
      }
      
    • 所以可以发现其实把结构体变量定义为全局变量还是会复制结构体中大量变量的值到堆栈中,但是比局部变量好一点,就是不用再main函数中再开辟一个内存来存储结构体类型变量,然后再复制一份入栈。因为定义为全局变量的结构体变量在编译时就已经分配好内存了,后面就不用在我们写的代码函数中再花内存和时间去存储了

  • 后面学了指针直接传地址也可以解决这个问题

六、作业

  • 定义一个结构体Gamer用来存储一个游戏中的角色的信息,包括血值、等级、坐标等信息。要求:

    • (1) 具体包含哪些信息自由设计;
    • (2) 但这些包含的类型中,必须要有一个成员是结构体类型
  • 定义一个函数,用来给这个结构体变量赋值

  • 定义一个函数,用来显示这个结构体变量的所有成员信息

    #include "stdafx.h"
    struct pet{
        int num;
    	short age;
    };
    struct role{
        int age;
        char name[10];
        int level;
        short blood;
        pet p;
    };
    
    role r; //全局变量,所以r结构体当中的所有成员默认初始值为0
    
    role Func(role r){  //赋值
        r.age = 18;
        r.name[0] = 'z';
    	r.name[1] = 'j';
        r.level = 100;
        r.blood = 20;
        r.p.num = 12;
        r.p.age = 3;
        return r;
    }
    
    void Func2(role r){  //输出
        printf("%d\n",r.age);
        for(int i = 0;r.name[i] != 0x0;i++){
            printf("%c",r.name[i]);
        }
        printf("\n%d\n",r.level);
        printf("%d\n",r.blood);
        printf("%d\n",r.p.num);
        printf("%d\n",r.p.age);
    }
    
    void main(int argc,char* argv[]){
        role r1 = Func(r);
        Func2(r1);
        getchar();  //暂停看结果
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值