文章目录
[1. 结构体类型的声明](#1. 结构体类型的声明)
[1.1 结构体回顾](#1.1 结构体回顾)
[1.1.1 结构体的声明](#1.1.1 结构体的声明)
[1.2 结构体变量的创建和初始化](#1.2 结构体变量的创建和初始化)
[1.3 匿名结构体类型](#1.3 匿名结构体类型)
[1.4 结构体的自引用](#1.4 结构体的自引用)
[2. 结构体内存对齐](#2. 结构体内存对齐)
[2.1 对齐规则](#2.1 对齐规则)
[2.2 练习与分析](#2.2 练习与分析)
[2.3 为什么存在内存对齐?](#2.3 为什么存在内存对齐?)
[2.4 修改默认对齐数](#2.4 修改默认对齐数)
[3. 结构体传参](#3. 结构体传参)
[4. 结构体实现位段](#4. 结构体实现位段)
[4.1 什么是位段?](#4.1 什么是位段?)
[4.2 位段的内存分配](#4.2 位段的内存分配)
[4.3 位段的应用](#4.3 位段的应用)
[4.4 位段使用的注意事项](#4.4 位段使用的注意事项)
引言
在C语言中,结构体是一种非常重要的自定义数据类型,它允许我们将不同类型的数据组合成一个整体,为复杂数据的组织和管理提供了极大的便利。本文将全面讲解结构体的声明、使用、内存对齐规则、传参方式以及位段的应用,帮助你深入理解结构体的底层原理和实际应用。
1. 结构体类型的声明
1.1 结构体回顾
结构体是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.1.1 结构体的声明
cpp
struct tag {
member-list;
} variable-list;
示例:描述一个学生
cpp
struct Stu {
char name[20]; // 名字
int age; // 年龄
char sex[5]; // 性别
char id[20]; // 学号
}; // 分号不能丢
1.2 结构体变量的创建和初始化
cpp
#include <stdio.h>
struct Stu {
char name[20];
int age;
char sex[5];
char id[20];
};
int main() {
// 按照结构体成员的顺序初始化
struct Stu s1 = {"张三", 20, "男", "20230818001"};
// 按照指定的顺序初始化
struct Stu s2 = {.age = 18, .name = "李四", .id = "20230818002", .sex = "女"};
printf("s1: %s, %d, %s, %s\n", s1.name, s1.age, s1.sex, s1.id);
printf("s2: %s, %d, %s, %s\n", s2.name, s2.age, s2.sex, s2.id);
return 0;
}
1.3 匿名结构体类型
在声明结构体时可以省略标签(tag),这称为匿名结构体类型:
cpp
// 匿名结构体类型
struct {
int a;
char b;
float c;
} x;
struct {
int a;
char b;
float c;
} a[20], *p;
注意 :编译器会将上面的两个声明当作完全不同的两个类型,因此 p = &x; 是非法的。匿名结构体类型如果没有重命名,基本上只能使用一次,同时只有在声明时才能创建变量,其他情况下无法创建变量。
1.4 结构体的自引用
在结构体中包含一个类型为该结构本身的成员(用于链表等数据结构):
cpp
// 错误方式:会导致无限大小
struct Node {
int data;
struct Node next; // 错误!
};
// 正确方式:使用指针
struct Node {
int data;
struct Node* next; // 正确
};
第一种方式相当于无限的嵌套,会导致无限大小,我们选择创建一个对应的结构体指针,指向下一个节点,这样就使得两个节点之间产生了联系。
注意:使用typedef时要避免匿名结构体自引用的问题:
cpp
// 错误方式
typedef struct {
int data;
Node* next; // 错误:Node尚未定义
} Node;
// 正确方式
typedef struct Node {
int data;
struct Node* next; // 使用完整的类型名
} Node;
2. 结构体内存对齐
结构体的大小计算是C语言中的一个重要考点,涉及内存对齐规则。
2.1 对齐规则
-
结构体的第一个成员对齐到结构体变量起始位置偏移量为0的地址处
-
其他成员变量要对齐到某个数字(对齐数)的整数倍地址处
-
对齐数 = 编译器默认对齐数与该成员大小的较小值
-
VS中默认对齐数为8,Linux中gcc没有默认对齐数
-
-
结构体总大小为最大对齐数(所有成员中对齐数的最大值)的整数倍
-
如果嵌套结构体,嵌套的结构体成员对齐到自己的最大对齐数的整数倍处,结构体整体大小是所有最大对齐数(包括嵌套结构体成员的对齐数)的整数倍
2.2 练习与分析
cpp
#include <stdio.h>
// 练习1
struct S1 {
char c1;
int i;
char c2;
};
// 练习2
struct S2 {
char c1;
char c2;
int i;
};
// 练习3
struct S3 {
double d;
char c;
int i;
};
// 练习4 - 结构体嵌套
struct S4 {
char c1;
struct S3 s3;
double d;
};
int main() {
printf("sizeof(struct S1) = %zu\n", sizeof(struct S1)); // 12
printf("sizeof(struct S2) = %zu\n", sizeof(struct S2)); // 8
printf("sizeof(struct S3) = %zu\n", sizeof(struct S3)); // 16
printf("sizeof(struct S4) = %zu\n", sizeof(struct S4)); // 32
return 0;
}
内存布局分析(以S1为例,假设int为4字节):
-
c1:偏移0,大小1字节 -
i:对齐数为4,需要对齐到4的倍数,偏移4,大小4字节 -
c2:对齐数为1,偏移8,大小1字节 -
总大小需要是最大对齐数(4)的整数倍,9→12字节
2.3 为什么存在内存对齐?
-
平台原因(移植性):不是所有硬件都能访问任意地址的任意数据,某些硬件平台只能在特定地址访问特定类型数据
-
性能原因:对齐的内存访问只需一次内存操作,未对齐的内存可能需要两次访问
空间优化技巧:将占用空间小的成员尽量集中在一起,可以节省空间。
2.4 修改默认对齐数
cpp
#include <stdio.h>
#pragma pack(1) // 设置默认对齐数为1
struct S {
char c1;
int i;
char c2;
};
#pragma pack() // 恢复默认对齐数
int main() {
printf("%zu\n", sizeof(struct S)); // 6(1+4+1)
return 0;
}
3. 结构体传参
结构体传参时,应优先传递指针而非结构体本身,以节省栈空间和时间开销。
cpp
#include <stdio.h>
struct S {
int data[1000];
int num;
};
// 传值:复制整个结构体,开销大
void print1(struct S s) {
printf("%d\n", s.num);
}
// 传址:只传递指针,高效
void print2(struct S* ps) {
printf("%d\n", ps->num);
}
int main() {
struct S s = {{1, 2, 3, 4}, 1000};
print1(s); // 传值,不推荐
print2(&s); // 传址,推荐
return 0;
}
结论:结构体传参时,应传递结构体的地址。
4. 结构体实现位段
4.1 什么是位段?
位段的声明与结构体类似,但有两个不同:
-
位段成员必须是整型(int、unsigned int、signed int或char)
-
位段成员名后有一个冒号和一个数字,表示该成员占用的位数
cpp
struct A {
int _a:2; // 使用2个bit
int _b:5; // 使用5个bit
int _c:10; // 使用10个bit
int _d:30; // 使用30个bit
};
4.2 位段的内存分配
cpp
struct S {
char a:3;
char b:4;
char c:5;
char d:4;
};
int main() {
printf("sizeof(struct S) = %zu\n", sizeof(struct S)); // 3字节
struct S s = {0};
s.a = 10; // 10的二进制1010,但只有3位,取010=2
s.b = 12; // 12的二进制1100,但只有4位,取1100=12
s.c = 3; // 3的二进制11,但只有5位,取00011=3
s.d = 4; // 4的二进制100,但只有4位,取0100=4
return 0;
}
位段的特性:
-
空间按需以4字节(int)或1字节(char)方式开辟
-
位段涉及很多不确定因素,不具可移植性
-
位段成员在内存中的分配顺序(从左到右或从右到左)标准未定义
-
当一个结构体包含两个位段,第二个位段较大无法容纳于第一个位段剩余位时,是否舍弃剩余位不确定
4.3 位段的应用
位段能够有效节省空间,在网络协议等场景中广泛应用。例如IP数据报格式中很多属性只需几个bit位就能描述,使用位段可以减少数据报大小,提高网络传输效率。
4.4 位段使用的注意事项
-
不能取地址:位段的几个成员可能共享一个字节,有些成员的起始位置不是字节起始位置,这些位置没有地址
-
不能直接使用scanf输入:需要先输入到变量,再赋值给位段成员
cpp
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;
}
总结

使用建议:
-
结构体设计:考虑内存对齐,将小成员集中放置以节省空间
-
结构体传参:传递指针而非结构体本身
-
位段使用:仅在需要节省空间且不关心可移植性时使用
-
跨平台开发:避免使用位段,或明确处理平台差异
掌握结构体的原理和使用技巧,能够帮助你编写更高效、更健壮的C语言程序,特别是在系统编程、嵌入式开发和网络编程等领域。
欢迎在评论区交流讨论,如果觉得有帮助,请点赞收藏支持!
更多C语言技术文章,请访问我的博客主页:我能坚持多久-CSDN博客