掌握C语言结构体:从入门到精通

一、结构体基础

1、基本概念

        C语言提供了众多的基本类型,但现实生活中的对象一般都不是单纯的整型、浮点型或字符串,而是这些基本类型的综合体。比如一个学生,典型地应该拥有学号(整型)、姓名(字符串)、分数(浮点型)、性别(枚举)等不同侧面的属性,这些所有的属性都不应该被拆分开来,而是应该组成一个整体,代表一个完整的学生。

在C语言中,可以使用结构体来将多种不同的数据类型组装起来,形成某种现实意义的自定义的变量类型。结构体本质上是一种自定义类型。

  • 结构体的定义:
struct 结构体标签
{
    成员1;
    成员2;
    ...
};
  • 语法:

    • 结构体标签,用来区分各个不同的结构体。
    • 成员,是包含在结构体内部的数据,可以是任意的数据类型。
  • 示例代码:
// 定义了一种称为 struct node 的结构体类型
struct node
{
    int a;
    char b;
    double c;  
};

int main()
{
    // 定义结构体变量
    struct node n;
}

2、 结构体初始化

  • 说明:
    • 结构体跟普通变量一样,涉及定义、初始化、赋值、取址、传值等等操作,这些操作绝大部分都跟普通变量别无二致,只有少数操作有些特殊性。这其实也是结构体这种组合类型的设计初衷,就是让开发者用起来比较顺手,不跟普通变量产生太多差异。
  • 结构体的定义和初始化:
    • 由于结构体内部拥有多个不同类型的成员,因此初始化采用与数组类似的列表方式。
    • 结构体的初始化有两种方式:①普通初始化;②指定成员初始化。
    • 为了能适应结构体类型的升级迭代,一般建议采用指定成员初始化。
  • 指定成员初始化的好处:
    • 成员初始化的次序可以改变
    • 可以初始化一部分成员
    • 结构体新增了成员之后初始化语句仍然可以使用
// 1,普通初始化
struct node n = {100, 'x', 3.14};

// 2,指定成员初始化
struct node n = {
                 .a = 100,  // 此处,小圆点.被称为成员引用符
                 .b = 'x',
                 .c = 3.14
                }

3、结构体的引用

  • 说明:结构体相当于一个集合,内部包含了众多的成员,每个成员实际上都是独立的变量,都可以被独立地引用,引用结构体成员非常简单,只需要使用一个成员引用符即可:
结构体普通变量:结构体名字.成员
结构体指针变量:结构体名字->成员
  • 示例代码
  // 主函数
int main(int argc, char const *argv[])
{ 
     // (2)、结构体的引用
    // 1、非字符串的赋值
    stu1.age   = 20;
    stu1.score = 60;

    // 2、字符串的赋值
    // stu1.phone = "123456";       // 错误,这句话的意思哦,将字符串"123456"的首字符的地址,赋值给数组(但是数组名不可被赋值)
    // 报错误:assignment to expression with array type(赋值的表达式的类型是数组类型) 
    strcpy(stu1.phone, "123456");   // 正确,通过strcpy函数,将字符串常量复制到数组的内存空间中 
}

4、 结构体指针与数组

跟普通变量别无二致,可以定义指向结构体的指针,也可以定义结构体数组。

  • 结构体指针:一般来说,结构体会包含比较多成员,结构体变量的尺寸会比一般的变量大,所以结构体作为参数传入函数的时候,最好用地址,这样效率高。(在传参时,如果参数为结构体,使用结构体指针传址)
// ->:指向结构体的成员变量
Student_t st;
Student_t *sp = &st;    
sp->age = 10;    // 注意:sp是指针(Student_t *sp;)
  • 结构体数组:
// 结构体数组:以下arr[10]一次性可以初始化10个结构体
struct student arr[10] = 
{
    {"zhangsan", 20, 'M', {2005, 1, 11}},
    {"lisi", 18, 'W', {2007, 2, 22}}
};

二、结构体尺寸

1、地址对齐

  • 说明:CPU字长确定之后,相当于明确了系统每次存取内存数据时的边界,以32位系统为例,32位意味着CPU每次存取都以4字节为边界,因此每4字节可以认为是CPU存取内存数据的一个单元。
  • 例子:
    • 如果存取的数据刚好落在所需单元数之内,那么我们就说这个数据的地址是对齐的,
    • 如果存取的数据跨越了边界,使用了超过所需单元的字节,那么我们就说这个数据的地址是未对齐的。
  • 地址未对其情况

  • 地址对其情况

        综上:从图中可以明显看出,数据本身占据了8个字节,在地址未对齐的情况下,CPU需要分3次才能完整地存取完这个数据,但是在地址对齐的情况下,CPU可以分2次就能完整地存取这个数据。如果一个数据满足以最小单元数存放在内存中,则称它地址是对齐的,否则是未对齐的。地址对齐的含义用大白话说就是1个单元能塞得下的就不用2个;2个单元能塞得下的就不用3个。
如果发生数据地址未对齐的情况,有些系统会直接罢工,有些系统则降低性能。

2、结构示例框架:

struct node
{
    char a;    // 1字节
    int b;     // 4字节
    char c;    // 1字节
}n;            // 6字节? -> sizoef(n); -> 12字节

(1)结构体的各成员变量的内存布局问题

  • 以定义时各成员变量出现的次序,依次保存。
  • 结构体的大小需要地址对齐(结构体中每个成员变量在内存中的存放位置需要对齐)

(2)为何需要地址对齐。

  • 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  • 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
  • 总体来说:结构体的内存对齐是拿空间来换取时间的做法。目的是提高CPU访问内存数据的效率

(3)地址对齐总结:

  • 结构体定义的时候,变量成员的顺序会影响结构体的大小;
  • 对齐:成员变量以什么样的方式排列;紧密排列、还是松散中间是不是有间隔;
  • 结构体中地址对齐的字节数 按 最大个成员的基本数据类型大小对齐;
  • 结构体的总大小为其对齐方式的整数倍;

3、结构尺寸说明:

        普通变量的m值:以32位系统为例,由于CPU存取数据总是以4字节为单元,因此对于一个尺寸固定的数据而言,当它的地址满足某个数的整数倍时,就可以保证地址对齐。这个数就被称为变量的m值。根据具体系统的字长,和数据本身的尺寸,m值是可以很简单计算出来的。

  • 示例:
char   c; // 由于c占1个字节,因此c不管放哪里地址都是对齐的,因此m=1
short  s; // 由于s占2个字节,因此s地址只要是偶数就是对齐的,因此m=2
int    i; // 由于i占4个字节,因此只要i地址满足4的倍数就是对齐的,因此m=4
double f; // 由于f占8个字节,因此只要f地址满足4的倍数就是对齐的,因此m=4

printf("%p\n", &c); // &c = 1*N,即:c的地址一定满足1的整数倍
printf("%p\n", &s); // &s = 2*N,即:s的地址一定满足2的整数倍
printf("%p\n", &i); // &i = 4*N,即:i的地址一定满足4的整数倍
printf("%p\n", &f); // &f = 4*N,即:f的地址一定满足4的整数倍
  • 注意,变量的m值跟变量本身的尺寸有关,但它们是两个不同的概念。
    • 概念(结构体尺寸):
      • 结构体的M值,取决于其成员的m值得最大值,即:M = max{m1, m2, m3...}
      • 结构体得地址和尺寸,都必须等于M值得整数倍
    • 技巧:
    1、先算出结构体成员加起来得理论大小值      --- 结构体得大小最低不能低过这个值
    2、再算出结构体成员m值得最大值            --- 结构体的大小是m值得倍数
    3、算出结构体结合理论和m值后的最小倍数
    4、考虑到内存的存储方式最终得出结构体大小

    • 示例代码:
    #include <stdio.h>
    
    // 一、32位系统
    struct node1
    {
        char c;     // 尺寸1字节,m值1
        short a;    // 尺寸2字节,m值2
        double b;   // 尺寸8字节,m值4
    };  // 理论:1+2+8 == 11; 结构体的m值:4; 结合理论和m值的最小倍数:12; 考虑到内存的存储方式(c、a:4  b:8 == 12)
    
    struct node2
    {
        char c;     // 尺寸1字节,m值1
        int b;      // 尺寸4字节,m值4
        short a;    // 尺寸2字节,m值2
    };  // 理论:1+4+2 == 7; 结构体的m值:4; 结合理论和m值的最小倍数:8; 考虑到内存的存储方式(c:4  b:4  a:4 == 12)
    
    
    struct node3
    {
        char c;     // 尺寸1字节,m值1
        int d;      // 尺寸4字节,m值4
        double e;   // 尺寸8字节,m值4
        short a;    // 尺寸2字节,m值2
        float b;    // 尺寸4字节,m值4
    };  // 理论:1+4+8+2+4 == 19; 结构体的m值:4; 结合理论和m值的最小倍数:20; 考虑到内存的存储方式(c:4  d:4  e:8 a:4 b:4 == 24)
    
    struct node4
    {
        char t:4;               // 尺寸4位,m值1(m值最低是1)
        char k:4;               // 尺寸4位,m值1(m值最低是1)
        unsigned short i:8;     // 尺寸8位(1字节),m值2
        unsigned long m;        // 尺寸4字节,m值4
    
    };  // 理论:0.5+0.5+1+4 == 6; 结构体的m值:4; 结合理论和m值的最小倍数:8; 考虑到内存的存储方式(t、k、i:4  m:4 == 8)
    
    struct node5
    {
        char dog[11];           // 尺寸11字节,m值1
        unsigned long cat;      // 尺寸4字节,m值4
        short pig;              // 尺寸2字节,m值2
        char fox;               // 尺寸1字节,m值1
    
    };  // 理论:11+4+2+1 == 18; 结构体的m值:4; 结合理论和m值的最小倍数:20; 考虑到内存的存储方式(dog:12、 cat:4 pig、fox:4 == 20)
    
    // 主函数
    int main(int argc, char const *argv[])
    {
        // 结构体大小的计算:
        printf("struc node1 的结构体的大小为 == %lu字节\n", sizeof(struct node1));
        printf("struc node2 的结构体的大小为 == %lu字节\n", sizeof(struct node2));
        printf("struc node3 的结构体的大小为 == %lu字节\n", sizeof(struct node3));
        printf("struc node4 的结构体的大小为 == %lu字节\n", sizeof(struct node4));
        printf("struc node5 的结构体的大小为 == %lu字节\n", sizeof(struct node5));  
    
        return 0;
    }

    三、结构体的可移植性

    • 说明:可移植性指的是相同的一段数据或者代码,在不同的平台中都可以成功运行
    • 问题:对于数据来说,有两方面会导致不可移植
      • 问题1:数据的尺寸发生改变
      • 问题2:数据位置发生改变
    • 原因:
      • 原因1:起因是基本的数据类型在不同的系统所占据的字节数不同造成的,解决方法是可移植性数据类型即可

    四、共用体(联合体)

    • 概念:几个不同的变量共用使用同一段内存的结构,在C语言中,被称为"共用体"类型结构。

    • 定义共用体类型
      union 共用体标签名
      {
          成员类型1  成员名1;
          成员类型2  成员名2;
           ...
      };
      /* ********示例********* */
      union A
      {
          char arr[6];
          int num;
          double d;
      };
    • 特点:
      • 联合体中的所有成员共享同一段内存的
      • 联合体中的最大个数据成员的大小就是联合体的大小
    • 结构体与共用体的区别
      • 结构体变量所占内存长度是各成员占的内存长度之和,每个成员分别占有其自己的内存单元。
      • 共用体变量所占的内存长度等于最长的成员的长度。共用体的内存开销要小一点。
      • 在共用体所用的内存中已经写入了数据,当使用其它元素时上次使用的内容将被覆盖。 也就是说他使几个不同类型的变量共占一段内存(相互覆盖),每次只有一个能使用。结构体则不然, 每个成员都会有存储空间的,可以一起用,内部变量间是相互独立的。

    拓展:内存中存储顺序(大小端模式)

    • 概念表示一个多字节存储单元的低地址存储数据的低有效位还是高有效位(从高地址开始还是从低地址开始)。
    • 小端序模式:低地址存放数据的低有效位,高有效位字节存储在内存的高地址处;(低存低,高存高)
    • 例如:一个4字节的整数0x12345678在内存中的存储顺序为:0x78(低地址), 0x56, 0x34, 0x12(高地址)
    • 大端序模式:高地址存放数据的低有效位,低有效位字节存储在内存的高地址处;(低存高,高存低)

    • 为何会有大小端模式
      • 以前不同的芯片公司在处理把寄存器的数据存放到内存中时,采用的方式不统一,才产生了大小端两种模式。(现在大部分系统都是使用小端序的。在当前主流的计算机体系中,小端序(Little-Endian)是主要的字节序列顺序,占据了统治地位。小端序系统在硬件设计制造上更为容易,并且天然契合人的右侧性习惯‌)

    • 大小端序模式的优势:
      • 小端序在硬件设计制造上更为容易‌。
      • 小端序使得内存的物理编组和读写更为直接和高效,内存管理‌更方便‌。
      • 大端序的存储方式更符合人类的阅读习惯,因为人类通常从左到右阅读。
      • 对于有符号数,大端序将符号位存储在最低地址处。
      • 大端序被广泛应用于网络协议中,被称为网络字节序。
    • 如何判断系统是大端序还是小端序
      • 方法一:直接访问地址判断(首先定义了一个无符号整数变量num,并将其赋值为1。然后,将num的地址强制转换为char类型的指针p,这样就可以通过p来访问num的每个字节。如果num的第一个字节(即最低有效字节)的值为1,则说明系统是小端序;否则,系统是大端序。)
    #include <stdio.h>
    
    int main() {
        unsigned int num = 1;   // 0x00 00 00 01
        /*
       数据:高有效位 -> 低有效位
            0x00 0x00 0x00 0x01
        地址:高地址 <- 低地址
            0xff..03 0xff..02 0xff..01 0xff..00
        */
    
        // 将 unsigned int 类型的指针转换为 char 类型的指针
        char *p = (char *)&num; // 获取 num 的最低有效字节
        
        // 检查最低地址有效位是否为 1
        if (*p == 1) {          // 如果低地址上的值为 1,则说明低地址存储低有效字节数据(低存低)
            printf("Little Endian\n");  // 低地址存低有效字节,小端序
        }
        else {                  // 如果低地址上的值为 0,则说明低地址存储高有效字节数据(低存高)
            printf("Big Endian\n");     // 低地址存高有效字节,大端序
        }
    }
    • 图解:

    • 方法二:通过联合体打印
    #include <stdio.h>
    
    // 联合体共用同一片内存空间
    union data{
        int a;
        char b;
    }; 
    
    int main()
    {
        union data myData;        // 定义一个联合体变量
        myData.a = 0x12345678;    // a是int4字节,将数据0x12345678分段存放在4字节空间中
        printf("%x\n",myData.b);  // b是char1字节,通过输出联合体b的值,访问一个字节空
        // 输出大小端序判断结果
        if (myData.b == 0x78) // 判断b的值是否为0x78,如果是则表示是小端序
        {
            printf("Little Endian\n"); 
        }
        else
        {
            printf("Big Endian\n");
        }
    }
    • 如何转换大小端序:
    #include <stdio.h>
     
    // 判断当前系统的字节序
    int if_endian() 
    {
        unsigned int x = 1;
        char *c = (char*)&x;
        // 如果低地址存储的是1,则为小端字节序
        return (*c == 1);
    }
     
    // 字节序转换函数
    unsigned int swap_endian(unsigned int num) 
    {
        unsigned int result = 0;
        result |= (num & 0x000000FF) << 24;
        result |= (num & 0x0000FF00) << 8;
        result |= (num & 0x00FF0000) >> 8;
        result |= (num & 0xFF000000) >> 24;
        return result;
    }
     
    int main() {
        // 测试用例
        unsigned int num = 0x12345678;
        
        // 判断当前机器的字节序
        if (if_endian()) 
        {
            printf("小端序\n");
        } else 
        {
            printf("大端序\n");
        }
        
        printf("原数据: 0x%X\n", num);
        
        // 字节序转换
        unsigned int swap_num = swap_endian(num);
        printf("转换后数据: 0x%X\n", swap_num);
        
        return 0;
    }
    

            本节内容到这里就结束了,C语言的知识点也到此为止了,请大家点赞关注收藏三连,跟着我的步伐进入数据结构的学习,有什么问题也可以在评论区留言。

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值