当前位置: 首页 > news >正文

C语言第十二章自定义类型:结构体

一.结构体类型的声明

1.结构体的概念

        结构体是程序员自定义的数据类型,允许将不同类型的数据组合在一起。结构体是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

2.结构体的声明

struct tag

{

   member-list;

}  variable-list;

        上述形式就是结构体的声明格式,其中struct tag为结构体类型;tag为结构体标签;struct为结构体的关键字;member-list为成员列表,用于创建不同数据类型的内容;variable-list为结构体变量名,表示用户用结构体类型创建的变量。下面举一个例子,创建一个学生结构体:

struct Stu
{char name[20];//名字 int age;//年龄 char sex[5];//性别char id[20];//学号 
}; //分号不能丢 

        上述代码创建了struct Stu类型的结构体,其中的成员变量有学生的名字、年龄、性别和学号。这里需要注意的是:在创建结构体时,大括号外的分号不可以舍去。

3.结构体变量的创建和初始化

#include <stdio.h>
struct Stu
{char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 
}s3;
int main()
{//按照结构体成员的顺序初始化 struct Stu s1 = { "张三", 20, "男", "20230818001" };printf("name: %s\n", s1.name);printf("age : %d\n", s1.age);printf("sex : %s\n", s1.sex);printf("id : %s\n", s1.id);//按照指定的顺序初始化 struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = 
"⼥" };printf("name: %s\n", s2.name);printf("age : %d\n", s2.age);printf("sex : %s\n", s2.sex);printf("id : %s\n", s2.id);return 0;
}

    上述代码首先创建了关于学生的结构体,内部的成员有:名字、年龄、性别和学号。然后紧随其后创建了全局的结构体变量s3,这里在括号后面的分号前创建的就是结构体变量,因为该结构体变量是在大括号外部创建的,所以该变量为全局变量。

        进入mian函数,首先创建了局部结构体变量s1,并进行了顺序初始化。对应着结构体成员的位置进行一一对应的顺序初始化。接下来打印操作用到的是结构体成员访问操作符:‘.’ ,对结构体成员进行访问,并打印初始化成功的结构体变量s1的结构体成员。

        下来的结构体变量s2的初始化属于指定顺序的初始化,根据结构体成员放位操作符和指定的顺序进行内容的初始化。

4.结构体的特殊声明

        在结构体声明的过程中,我们可以进行结构体的不完全创建。此时的声明就是结构体的特殊声明,该结构体的类型也被称为匿名结构体类型。

//匿名结构体类型 
struct
{int a;char b;float c;
} x;
struct
{int a;char b;float c;
} a[20], *p;

        上面两种结构体的声明形式均为特殊声明,均没有结构体正常声明的结构体标签,只有结构体的关键字。那么问题来了?

         在上面代码的基础上,下面的代码合法吗?

         p = &x;

结果告诉我们:不合法!原因是:编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。 匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。

5.结构体的自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?

比如定义一个链表的节点:

struct Node
{int data;struct Node next;
};

        上述代码正确吗?如果正确,那sizeof(struct Node) 是多少?

上述代码是错误的,因为一个结构体中再包含一个同类型的结构体变量,就像病态的套娃一样,一个结构体内部竟然内放的是和自身一样大的结构体和一个数据,这显然不正确。结构体变量的大小就是无穷大,是不合理的。 正确的自引用方式如下:

struct Node
{int data;struct Node* next;
};

        上述结构体的自引用操作为正确的,因为一个结构体内部储存了数据和一个指针变量,这显然是符合常理的。

        在结构体自引用的过程中,夹杂了typedef对匿名结构体类型重命名,也容易引入问题,看看 下面的代码,可行吗?

typedef struct
{int data;Node* next;
}Node;

        答案是不行的,因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的。

        解决方案如下:定义结构体不要使用匿名结构体。

typedef struct Node
{int data;struct Node* next;
}Node;

二.结构体的内存对齐

        理解了结构体的声明,下来我们就该学习计算结构体的大小:结构体的内存对齐。

1.内存对齐规则

        (1)结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。

        其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

对齐数=编译器默认的一个对齐数与该成员变量大小的较小值。

- VS 中默认的值为 8

- Linux中 gcc没有默认对齐数,对齐数就是成员自身的大小

下面的习题以VS为例。

        结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大数)的整数倍。

        如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

        接下来进行结构体大小的练习:

//练习1 
struct S1
{char c1;int i;char c2;
};
printf("%d\n", sizeof(struct S1));

//练习2 
struct S2
{char c1;char c2;int i;
};
printf("%d\n", sizeof(struct S2));

//练习3 
struct S3
{double d;char c;int i;
};
printf("%d\n", sizeof(struct S3));

//练习4-结构体嵌套问题 
struct S4
{char c1;struct S3 s3;double d;
};
printf("%d\n", sizeof(struct S4));

        上述四个练习和解答图片均为结构体大小的计算练习,在上述四张图解中,不同颜色表示不同变量的存储位置,一个小黑框表示一个字节。其中灰色颜色的框框表示该字节的内存为了内存对齐而浪费掉了。

2.为什么存在内存对齐?

        数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于:为了访问未对齐的内存,处理器需要两次内存访问,而对齐的内存访问仅需要一次访问。内存对齐是为了提高计算机系统的性能和效率。现代计算机的CPU在访问内存时,通常以特定大小的块(通常为4字节或者8字节)为单位进行读取。如果没有内存对齐,CPU可能需要多次访问内存才能获取完整数据,从而导致性能下降。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问操作,因为该数据可能被分放在两个8字节内存块中。

        如果不存在内存对齐,用练习1进行举例:

struct S1
{char c1;int i;char c2;
};
printf("%d\n", sizeof(struct S1));

        上述两张图片分别为不存在内存对齐和存在内存对其的情况,首先计算机的CPU在访问内存时,通常以4字节或者8字节的内存块为单位进行内存的读取。如果计算机CPU以4字节的内存块为单位进行内存的读取。当存在内存对齐时,每次CPU读取内存都会完整的读取到一个变量的数据;但是不存在内存对齐时,计算机CPU以4字节内存单位块进行读取,有可能会读取到不完整的数据,这时还需要下一次内存读取才能获取该残缺的数据。

        所以这就是内存对齐的好处,结构体的内存对齐原理就是拿空间来换取时间。

3.修改默认对齐数

        现在我们知道了内存对齐的好处,知道了内存对齐是以空间换取时间和效率。那么如果我们既想要内存对齐,又要节省空间,该怎么做呢?

        一种方法是:让占用空间小的成员尽量集中在一起,另一种方法就是:修改默认对齐数。下面举第一种方法的例子:

//例如: 
struct S1
{char c1;int i;char c2;
};
struct S2
{char c1;char c2;int i;
};

        由于上述两种不同的结构体成员定义顺序,造成了结构体大小的不同。(在结构体成员不改变的前提下,将占用空间小的变量集中定义,会节省空间)。下面解答怎样修改默认的内存对齐数:

        #pragma 这个预处理指令,可以改变编译器的默认对齐数。下面给出使用的例子:

#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1 
struct S
{char c1;int i;char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认 
int main()
{printf("%d\n", sizeof(struct S));return 0;
}

             上述代码中,通过预处理指令对默认的内存对齐数进行修改,第一次使用,将默认的内存对齐数修改为1,第二次预处理目的是:取消第一次设置的对齐数1,重新还原为默认值。

        通过上述的修改默认对齐数的学习,结构体在内存对齐方式不合适的时候,我们可以自己更改默认对齐数。从而到达预期的目的。

三.结构体传参

struct S
{int data[1000];int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参 
void print1(struct S s)
{printf("%d\n", s.num);
}
//结构体地址传参 
void print2(struct S* ps)
{printf("%d\n", ps->num);
}
int main()
{print1(s); //传结构体 print2(&s); //传地址 return 0;
}

        上面的 print1 和 print2 函数哪个好些? 答案是:print2函数。

        原因: 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,当结构体过大时,参数压栈的的系统开销比较大,所以会导致性能的下降,效率不高。而传地址不会有此类问题。

         结论:结构体传参的时候,最好传递结构体的地址。

四.结构体实现位段

1.什么是位段?

        位段是C语言中的一种数据结构特性,允许在结构体中按位分配成员变量的存储空间。通过位段,可以精确控制每个成员占用的空间大小,从而节省内存空间。

        位段的声明方式是在结构体成员后添加冒号和位数(单位是比特位),例如:

struct A
{int _a:2;int _b:5;int _c:10;int _d:30;
};

        位段的特点有:

        1.节省空间:位段允许将多个成员变量压缩到一个整型变量中,减少内存的占用。

        2.不能对成员变量取地址:因为位段的允许多个成员压缩到一个字节中,在计算机中一个字节对应一个地址,所以当多个变量压缩在一个字节的空间时,就不可以对其成员变量进行取地址。

        A就是⼀个位段类型。那位段A所占内存的大小是多少?

 printf("%d\n", sizeof(struct A));

2.位段的内存分配

1. 位段的成员可以是 int、 unsigned int 、signed int 或者是 char 等类型。

2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应避免使用位段。

//⼀个例⼦ 
struct S
{char a:3;char b:4;char c:5;char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
//空间是如何开辟的? 

        上述图片解释了位段的空间开辟,因为位段是不跨平台的,所以在这里假设位段分配空间的顺序为从右向左;假设第一个位段剩余空间无法容纳第二个位段成员时,对剩余空间进行舍弃,开辟新的空间存放第二个位段成员;假设位段中一次最多开辟8个比特位。

        假设完成后就可以进行位段空间分配的分析了,首先开辟8个比特位,从右向左的3个比特位用于存放第一个位段成员初始化的值10的二进制补码,这时可以发现放不下,则只取10的二进制补码后3位放入开辟好的第一个位段空间中;此时剩余5个比特位的空间,对于存放第二个位段成员够用,所以先不开辟新的空间,继续在该空间存储第二个位段成员。将第二个位段成员12的二进制补码的后4位,从右向左存放在剩余空间处;此时空间只剩下最左边的1个比特位,明显不够存储第三个位段成员,所以此时从右段进行新空间的开辟(8个比特位)。然后从右向左存储第三个位段成员3的二进制补码的后5位;此时剩余的空间为3个比特位,不够存储最后一个位段成员,所以继续进行新空间的开辟(8个比特位),这时将最后一个位段成员4的二进制补码的后4位从右向左存储在新开辟的空间处。

        最后内存存储的具体数值为:01100010  00000011  00000100化为16进制的内存形式为:62  03  04。刚好等于上述图片的内存监视区的数值。

3.位段的跨平台问题

1. int 位段被当成有符号数还是无符号数是不确定的。

2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会 出问题,但是在32位机器却不会出现问题。)

3. 位段中的成员在内存中从左向右分配,还是从右向左分配,C语言标准尚未定义。

4. 当一个结构包含两个位段,第二个位段成员比较大,第一个位段剩余的空间无法容纳,是舍弃 剩余的位还是利用,这是不确定的。

总结: 跟结构体相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

4.位段的应用

        下图是网络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较小一些,对网络的畅通是有帮助的。

5.位段使用的注意事项

        位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。所以不能对位段的成员使用&操作符,这样就不能使用scanf函数直接对位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。如下代码所示:

struct A
{int _a : 2;int _b : 5;int _c : 10;int _d : 30;
};
int main()
{struct A sa = {0};scanf("%d", &sa._b);//这是错误的 //正确的⽰范 int b = 0;scanf("%d", &b);sa._b = b;return 0;
}
http://www.dtcms.com/a/348982.html

相关文章:

  • LangChain RAG系统开发基础学习之文档切分
  • Python核心技术开发指南(016)——表达式
  • 多线程——认识Thread类和创建线程
  • 【记录】Docker|Docker镜像拉取超时的问题、推荐的解决办法及安全校验
  • FPGA时序分析(四)
  • asio的线程安全
  • 使用Cobra 完成CLI开发 (一)
  • 3.1 存储系统概述 (答案见原书 P149)
  • C++ string自定义类的实现
  • 【论文阅读 | arXiv 2025 | WaveMamba:面向RGB-红外目标检测的小波驱动Mamba融合方法】
  • 上科大解锁城市建模新视角!AerialGo:从航拍视角到地面漫步的3D城市重建
  • 深度剖析Spring AI源码(三):ChatClient详解,优雅的流式API设计
  • R60ABD1 串口通信实现
  • 在 Ubuntu 24.04 或 22.04 LTS 服务器上安装、配置和使用 Fail2ban
  • 【Qwen Image】蒸馏版与非蒸馏版 评测小结
  • 第3篇:配置管理的艺术 - 让框架更灵活
  • 多线程下单例如何保证
  • [身份验证脚手架] 前端认证与个人资料界面
  • 2025.8.18-2025.8.24第34周:有内耗有挣扎
  • Spring Cloud 快速通关之Sentinel
  • 遥感机器学习入门实战教程|Sklearn案例⑩:降维与分解(decomposition 模块)
  • [e3nn] 等变神经网络 | 线性层o3.Linear | 非线性nn.Gate
  • 动态规划--编译距离
  • AI代码生成器全面评测:六个月、500小时测试揭示最强开发助手
  • Redis 高可用篇
  • 51单片机-实现定时器模块教程
  • GaussDB 数据库架构师修炼(十八) SQL引擎-统计信息
  • 用 WideSearch 思路打造「零幻觉、全覆盖」的多 Agent 信息收集器
  • SRE 系列(四)| MTTI 与 On-Call:高效故障响应之道
  • C++标准库算法:从零基础到精通