目录
一、结构体
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 位段的应用

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++的类型检查比较严格。
最后,感谢阅读,欢迎大家交流与指正。

286

被折叠的 条评论
为什么被折叠?



