三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

结构体_联合体_枚举

结构体_联合体_枚举

目录

一、结构体

1.1 结构体类型的声明

1.1.1 结构体声明和初始化

1.1.2 typedef重命名结构体

结构体自引用规则:

1.2 结构体内存对齐

1.2.1 对齐规则

1.2.2 修改默认对齐数

1.3 结构体传参

1.4 结构体实现位段

1.4.1 位段的内存分配

1.4.2 位段的跨平台问题

1.4.3 位段的应用

1.4.4 位段使用的注意事项

二、联合体

2.1 联合体类型的声明

2.2 联合体的内存特点

2.2.1 相同成员的结构体和联合体内存对比

2.3 联合体大小的计算

2.3.1 练习

三、枚举

3.1 枚举的声明

3.2 枚举类型的优点

3.3 枚举类型的使用


一、结构体

1.1 结构体类型的声明

含义:结构体是C语言的自定义聚合类型,用于将多个不同类型的数据打包为一个整体,变为一个新的类型。

1.1.1 结构体声明和初始化

struct 结构体标签名 { 类型名 成员名1; 类型名 成员名2; // ... }; // 末尾分号是语法强制要求,不可省略

例如描述一个学生:

// 先声明类型 struct Stu { char name[20]; int age; char sex[5]; }; // 后定义变量 struct Student s1; struct Student s2; // 或者声明变量的同时定义变量 struct Student { char name[20]; int age; char sex[5]; }s1,s2; // 直接在末尾定义变量

结构体的特殊声明:匿名结构体

struct { int a; char b; } x, y; // 仅此处可定义变量

结构体成员初始化(定义变量时赋值):

1. 初始化(定义变量时赋值)

//完全初始化 struct Stu s1 = {"zhangsan", 18}; //部分初始化,剩下置0 struct Stu s2 = {"lisi"}; //age = 0

2. 同类型结构体变量直接整体赋值✅

struct Stu s1 = {"zhangsan",18}; struct Stu s3; s3 = s1; //整体拷贝,把s1全部成员复制给s3

注:这是拷贝,不是共用内存,s3 修改不会影响 s1。

(结构体支持 = 直接赋值,数组不能直接用 = 整体赋值)

3. 逐个成员赋值

struct Stu s4; s4.age = 20; // s4.name = "wangwu"; ❌错误!数组名是地址,不能直接赋值 strcpy(s4.name, "wangwu"); //字符串要用strcpy拷贝

4.结构体指针访问成员

struct Stu s = {"zhang",18}; struct Stu* ps = &s; ps->age = 22; //指针用 -> strcpy(ps->name,"lisi");

常见错误汇总:

1.❌ s.name = "abc";

name 是 char 数组,char数组在结构体中不能直接赋值,必须用strcpy。

2.❌ 不同结构体类型不能互相赋值。

总结:同类型结构体变量可以直接=整体赋值,完成全部成员拷贝;内部字符串数组成员不能直接赋值,要用 strcpy。


观察下面有趣代码:

struct { int a; char b; float c; }x; struct { int a; char b; float c; }a[20],*p; // 在上面代码的基础上,下面的代码合法吗? p = &x;

警告:

1.编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。

2.匿名结构体类型,如果没有进行重命名的话,基本只能使用一次。

1.1.2 typedef重命名结构体

用 typedef 给结构体重命名,后续定义变量无需再写 struct 关键字。

写法1:无标签版(不支持自引用)

typedef struct { char a; int b; } t_t; // t_t 是新类型名 t_t s1; // 直接用别名定义变量

写法2:带标签版(支持自引用,链表 / 树必备)

typedef struct Node { int data; struct Node* next; // 自引用必须使用指针 } t_t; // 新类型名 t_t node1;
结构体自引用规则:

1.不能包含自身类型的普通成员(会导致大小无线循环,编译错误)。

2.只能包含指向自身类型的指针

自引用时,在匿名结构体自引用时提前用新类型创建成员变量,可行吗?

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

typedef 重命名的别名要等整个结构体声明完成后才会生效,在结构体内部,别名还未被定义,无法使用。

正确自引用:结构体必须带有标签,不使用匿名结构体。

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

1.2 结构体内存对齐

1.2.1 对齐规则

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

2.从第2个成员变量开始,都要对齐到某个对齐数的整数倍的地址处。

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

(VS的默认的值为 8)

(linux 中 gcc 没有默认的一个对齐数,对齐数就是成员自身的大小)

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

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

练习:

struct S1 { char c1; int i; cahr c2; }; struct S2 { char c1; cahr c2; int i; }; struct S3 { double d; char c; int i; }; struct S4 { char c1; struct S3 s3; double d; }; printf("%zu\n",sizeof(struct S1)); printf("%zu\n",sizeof(struct S2)); printf("%zu\n",sizeof(struct S3)); printf("%zu\n",sizeof(struct S4));

为什么结构体存在内存对齐?

简单说:结构体的内存对齐是拿空间来换取时间的做法。

在设计结构体时,为了既满足对齐,又节省空间,应该让占用空间小的成员尽量集中在一起

1.2.2 修改默认对齐数

利用 #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.3 结构体传参

#include<stdio.h> struct S { int arr[5] = {1,2,3,4,5}; int num = 100; }s; //传值调用 void print1(struct S t) { for(int i = 0;i < 5;i++) { printf("%d ",t.arr[i]); } printf("\n"); printf("%d\n",t.num); } //传址调用 void print2(const struct S* ps) { for(int i = 0;i < 5;i++) { printf("%d ",ps->arr[i]); } printf("\n"); printf("%d\n",ps->num); } int main() { print1(s); print2(&s); return 0; }

上述代码中,print1 和 print2 哪个更好?

print2 更好,原因如下:

1.函数传参时,参数是需要压栈,会有时间和空间上的开销。

2.如果传递一个结构题体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能下降。

总结:结构体传参的时候,要传结构体的地址。


1.4 结构体实现位段

概念:位段(也叫位域)是 C 语言基于结构体实现的内存精细化控制语法,允许手动指定结构体成员占用的二进制位数(bit);核心目的是节省内存,常用于硬件寄存器、协议报文等对内存极度敏感的底层场景。

位段和结构体的不同:

1.位段的成员必须是 int、unsigned int 或 signed int ,在C99中位段成员的类型也可以选择其他整型家族类型,比如:char。

2.位段的成员名后面有一个冒号和数字。

基础语法:

在结构体成员声明后加:位数,指定该成员占用的比特数。

struct 位段名 { 类型 成员名1 : 位数1; 类型 成员名2 : 位数2; // ... };

注:

1.成员类型通常为 unsigned int、int、unsigned char类型

2.位数必须是非负整数,不能超过对应类型的总比特数。

那么位段所占内存的大小是多少?

#include<stdio.h> int main() { struct A { int a:2; int b:5; int c:10; int d:30; }; printf("%zu\n",sizeof(struct A)); return 0; }

1.4.1 位段的内存分配

1.位段的成员可以是 int、undigned 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; //空间是如何开辟的?

如图:

1.4.2 位段的跨平台问题

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

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

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

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

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

1.4.3 位段的应用

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

1.4.4 位段使用的注意事项

1.位段成员不可取地址:内存最小寻址单位是字节,单个比特没有独立地址,因此不能对成员使用 & 取地址,也不能直接用 scanf 读入到位段成员。

2.大小不是比特数简单相加:位段以存储单元位为单位分配空间,剩余空间不足时会废弃并开启新单元,最终整体大小为存储单元大小的整数倍,不等于所有位段位数之和。

3.零长度位段特殊作用:unsigned int :0;是专属语法,作用是强制结束当前存储单元,后续位段从下一个全新存储单元开始,并非仅空出 0 比特。

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; }

二、联合体

2.1 联合体类型的声明

概念:联合体(也叫共用体,关键字 union),像结构体一样,也是由一个或者多个成员构成(这些成员可以是不同类型)。

基础语法:

union 联合体标签名 { 类型 成员名1; 类型 成员名2; // ... }; // 末尾分号为语法强制要求,不可省略

仅声明类型时不占用内存,定义变量时才分配实际内存空间。

四种常见的声明方式:

1. 先声明类型,后定义变量

union Data { char c; int i; double d; }; union Data d1, d2;

2. 声明类型的同时定义变量

union Data { char c; int i; double d; } d1, d2;

3. 匿名联合体

union { char c; int i; } d1;

4. typedef 重命名

typedef union { char c; int i; double d; } UData; UData d1; // 定义变量无需再写 union 关键字

1. 定义时初始化

❌不能直接初始化其他成员,只能对第一个成员初始化:

union Un u1 = {0x01}; //等价 u1.c = 0x01; union Un u = {.i=100}; //C99指定初始化可以,考试很少见

结构体与联合体在赋值方面对比:

相同点

1.同类型变量都支持直接 变量2 = 变量1 整体赋值,完整拷贝全部内存字节。

2.普通变量用.,指针用 -> 访问成员。

3.如果内部包含char[]数组:数组不能直接=赋值,要用 strcpy。

核心不同

结构体各个成员独立内存,给其中一个成员赋值,不会影响别的成员。


2.2 联合体的内存特点

内存分配规则:

1. 联合体总大小 >= 占用空间最大成员的大小(因为联合体至少得有能力保存最大的成员);并且联合体总体大小必须是所有成员中最大对齐数 的整数倍。

2. 起始地址一致:所有成员都从联合体的首地址开始存储,共享同一块内存起点。

观察内存:

#include<stdio.h> union UN { char c; int i; }; int main() { union UN un = {0}; un.i = 0x11223344; un.c = 0x55; printf("%x\n",un.i); return 0; }

上述代码发现,将i的第四个字节的内容修改为了55;un的内存局部图如下:

观察地址:

#include<stdio.h> union UN { char a; int b; }; int main() { union UN un = { 0 }; printf("&un = %p\n",&un); printf("&(un.a) = %p\n",&(un.a)); printf("&(un.b) = %p\n",&(un.b)); return 0; }

观察发现,上述代码三者的地址完全相同。

2.2.1 相同成员的结构体和联合体内存对比

//结构体 struct S { char c; int i; }; struct S s = {0}; //联合体 union Un { char c; int i; }; union Un un = {0};

内存对比图:


2.3 联合体大小的计算

1.联合体的大小至少是最大成员的大小。

2.当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

#include<stdio.h> union Un1 { char c[5]; int i; }; union Un2 { short c[7]; int i; }; int main() { printf("%zu\n",sizeof(union Un1)); printf("%zu\n",sizeof(union Un2)); return 0; }

2.3.1 练习

1. 某商场搞促销活动,要上线一个礼品兑换券,礼品兑换单中有三种商品:图书、杯子、衬衫。

每种商品信息如下:

图书:书名、作者、页数

杯子:设计

衬衫:设计、可选颜色、可选尺寸

利用联合体,归纳上述信息,并且使得所使用的空间尽可能的小:

struct gift_list { int stock_number;//库存量 double price; //定价 int item_type;//商品类型 union { struct { char title[20];//书名 char author[20];//作者 int num_pages;//⻚数 }book; struct { char design[30];//设计 }mug; struct { char design[30];//设计 int colors;//颜⾊ int sizes;//尺⼨ }shirt; }item; };

2. 利用联合体,判断当前机器是大端还是小端。

#include<stdio.h> union Un { char a; int b; }; int check_sys() { union Un un; un.b = 1; return un.a; } int main() { if(check_sys() == 1) { printf("小端\n"); }else { printf("大端\n"); } return 0; }

优化版:

int check_sys() { union { char a; int b; }un; un.b = 1; return un.a; }

三、枚举

含义:把可能的取值进行一一列举。

3.1 枚举的声明

enum 枚举名 { 枚举常量1, 枚举常量2, 枚举常量3 };

3.2 枚举类型的优点

我们可以使用 #define 定义常量,为什么非要使用枚举?

枚举的优点:

1.增加代码的可读性和可维护性。

2.和 #define 定义的标识符比较枚举有类型检查,更加严谨。

3.便于调试,预处理阶段会删除 #define 定义符号。

4.使用方便,一次可以定义多个变量。

5.枚举常量是遵循作用域规则的,枚举声明在函数内,只能在函数内使用。


3.3 枚举类型的使用

enum Color { RED = 1, GREED = 4, BLUE = 8 }; enum Color clr = GREED;

注:在C语言中是可以拿整数给变量赋值的,但是在C++中不行,C++的类型检查比较严格。


最后,感谢阅读,欢迎大家交流与指正。

← 返回列表