1. 结构体类型的声明
1.1 结构的声明
结构体(struct)是C语言中用于组合不同类型数据的复合数据类型。它允许我们将多个相关的变量打包成一个整体,便于管理和操作。
基本声明语法:
c
struct 结构体标签 {
类型1 成员1;
类型2 成员2;
// ... 更多成员
};
示例:
c
// 声明一个学生结构体
struct Student {
char name[20]; // 姓名
int age; // 年龄
float score; // 成绩
char id[10]; // 学号
};
1.1.1 结构体变量的创建和初始化
创建结构体变量有多种方式:
方式1:先声明结构体类型,再定义变量
c
struct Point {
int x;
int y;
};
int main() {
struct Point p1; // 定义变量,未初始化
struct Point p2 = {10, 20}; // 初始化所有成员
struct Point p3 = {.x = 5, .y = 15}; // 指定成员初始化(C99)
return 0;
}
方式2:声明结构体类型的同时定义变量
c
struct Student {
char name[20];
int age;
} stu1, stu2; // 直接定义两个变量
int main() {
strcpy(stu1.name, "张三");
stu1.age = 20;
return 0;
}
方式3:使用typedef创建类型别名
c
typedef struct {
char name[20];
int age;
float salary;
} Employee; // Employee现在是一个类型名
int main() {
Employee emp1 = {"李四", 30, 8000.0};
Employee emp2;
emp2.age = 25;
return 0;
}
1.2 结构的特殊声明:匿名结构体类型
匿名结构体是指在声明时省略了结构体标签,只能使用一次:
c
struct {
int x;
int y;
} point1, point2; // 只能创建这两个变量
// 错误示例:无法再创建新的变量
// struct point3; // 编译错误,类型未命名
匿名结构体的限制:
- 只能在声明时定义变量
- 不能在其他地方再次使用该类型
- 主要用于一次性使用的简单结构
1.3 结构的自引用
结构体自引用是指结构体包含指向自身类型的指针,常用于链表、树等数据结构:
c
// 正确的自引用方式
struct Node {
int data;
struct Node* next; // 指向同类型结构体的指针
};
// 错误示例:不能直接包含自身
// struct WrongNode {
// int data;
// struct WrongNode node; // 错误:大小无法确定
// };
// 使用typedef的正确方式
typedef struct ListNode {
int val;
struct ListNode* next; // 必须使用struct ListNode*
} ListNode;
重要提示:定义结构体时不要使用匿名结构体进行自引用
c
// 错误:匿名结构体无法自引用
typedef struct {
int data;
Node* next; // 错误:Node尚未定义
} Node;
// 正确:使用结构体标签
typedef struct Node {
int data;
struct Node* next;
} Node;
2. 结构体变量的创建和初始化
2.1 对齐规则
结构体的内存对齐是编译器为了优化内存访问速度而采用的策略。首先得掌握结构体的对齐规则:
规则详解:
1. 第一个成员对齐规则
- 结构体的第一个成员对齐到结构体变量起始位置偏移量为0的地址处。
2. 其他成员对齐规则
- 从第2个成员变量开始,都要对齐到某个对齐数的整数倍的地址处。
- 对齐数 = min(编译器默认对齐数, 该成员大小)
- VS中默认的值为8
- Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
3. 结构体总大小规则
- 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
4. 嵌套结构体的对齐规则
- 嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处
- 结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
示例分析:
c
#include <stdio.h>
struct Example1 {
char c; // 大小1,对齐数1(VS: min(8,1)=1)
int i; // 大小4,对齐数4(VS: min(8,4)=4)
double d; // 大小8,对齐数8(VS: min(8,8)=8)
};
struct Example2 {
int i; // 大小4,对齐数4
char c; // 大小1,对齐数1
double d; // 大小8,对齐数8
};
int main() {
printf("Example1大小: %zu\n", sizeof(struct Example1));
printf("Example2大小: %zu\n", sizeof(struct Example2));
return 0;
}
内存布局分析(假设在VS环境下):
struct Example1 内存布局:
偏移量 0: char c (1字节)
偏移量 1-3: 填充(为了对齐int)
偏移量 4-7: int i (4字节)
偏移量 8-15: double d (8字节)
总大小: 16字节
struct Example2 内存布局:
偏移量 0-3: int i (4字节)
偏移量 4: char c (1字节)
偏移量 5-7: 填充(为了对齐double)
偏移量 8-15: double d (8字节)
总大小: 16字节
2.2 为什么存在内存对齐?
结构体的内存对齐是拿空间来换取时间的做法,主要原因有:
- 平台兼容性:不是所有的硬件平台都能访问任意地址上的任意数据
- 性能优化:对齐的内存访问速度更快
- 硬件要求:某些处理器要求特定类型的数据必须从特定地址开始
在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到?
让占用空间小的成员尽量集中在一起
优化示例:
c
// 未优化的结构体:24字节
struct Unoptimized {
char c1; // 1字节
double d; // 8字节
char c2; // 1字节
int i; // 4字节
};
// 优化后的结构体:16字节
struct Optimized {
double d; // 8字节
int i; // 4字节
char c1; // 1字节
char c2; // 1字节
};
int main() {
printf("未优化大小: %zu\n", sizeof(struct Unoptimized));
printf("优化后大小: %zu\n", sizeof(struct Optimized));
return 0;
}
2.3 修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
c
#include <stdio.h>
#pragma pack(1) // 设置默认对齐数为1
struct S1 {
char c1; // 1字节
int i; // 4字节
char c2; // 1字节
};
#pragma pack() // 取消设置的对齐数,还原为默认
#pragma pack(4) // 设置默认对齐数为4
struct S2 {
char c1; // 1字节
int i; // 4字节
char c2; // 1字节
};
#pragma pack() // 取消设置的对齐数,还原为默认
// 默认对齐(VS中为8)
struct S3 {
char c1; // 1字节
int i; // 4字节
char c2; // 1字节
};
int main() {
printf("pack(1) - S1大小: %zu\n", sizeof(struct S1)); // 6
printf("pack(4) - S2大小: %zu\n", sizeof(struct S2)); // 12
printf("默认对齐 - S3大小: %zu\n", sizeof(struct S3)); // 12(VC)或12(gcc)
return 0;
}
3. 结构体内存对齐实战
c
#include <stdio.h>
// 大结构体示例
struct S {
int data[1000]; // 4000字节(假设int为4字节)
int num; // 4字节
};
// 值传递:复制整个结构体(低效)
void print1(struct S x) {
int i = 0;
for (i = 0; i < 6; i++) {
printf("%d ", x.data[i]);
}
printf("\n");
printf("%d\n", x.num);
}
// 指针传递:只传递地址(高效)
void print2(const struct S* p) {
int i = 0;
for (i = 0; i < 6; i++) {
printf("%d ", p->data[i]);
}
printf("\n");
printf("%d\n", p->num);
}
int main() {
struct S s = { {1, 2, 3, 4, 5, 6}, 100 };
printf("结构体大小: %zu 字节\n", sizeof(struct S));
printf("值传递结果:\n");
print1(s); // 传递整个结构体副本(4004字节)
printf("\n指针传递结果:\n");
print2(&s); // 只传递地址(4或8字节)
return 0;
}
关键结论:结构体传参的时候,要传结构体的地址。
原因:
- 效率高:传递指针(4或8字节)比传递整个结构体快
- 节省栈空间:避免在栈上复制大量数据
- 可修改性:如果需要修改原结构体,必须传递指针
4. 位段(Bit-field)
4.1 什么是位段
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是整型家族类型(int、unsigned int、signed int),在C99中位段成员的类型也可以选择其他整型家族类型,比如:char、short等。
- 位段的成员名后边有一个冒号和一个数字,表示该成员占用的位数。
c
struct BitField {
unsigned int a : 4; // a占用4位
unsigned int b : 5; // b占用5位
unsigned int c : 3; // c占用3位
unsigned int d : 20; // d占用20位
};
4.2 位段的内存分配
位段的内存分配规则:
- 位段的成员可以是int、unsigned int、signed int或char等整型
- 位段的空间是按照需要以4字节(int)或1字节(char)的方式来开辟的
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
内存分配示例:
c
#include <stdio.h>
struct A {
unsigned int a : 2; // 0-1位
unsigned int b : 5; // 2-6位
unsigned int c : 10; // 7-16位(需要第二个unsigned int)
unsigned int d : 30; // 0-29位(新的unsigned int)
};
int main() {
printf("struct A大小: %zu 字节\n", sizeof(struct A)); // 8字节(2个unsigned int)
struct A bitfield;
bitfield.a = 3; // 二进制: 11(最大3,因为只有2位)
bitfield.b = 31; // 二进制: 11111(最大31,5位)
bitfield.c = 1023; // 二进制: 1111111111(最大1023,10位)
bitfield.d = 0x3FFFFFFF; // 最大30位能表示的值
printf("a=%u, b=%u, c=%u, d=%u\n",
bitfield.a, bitfield.b, bitfield.c, bitfield.d);
return 0;
}
4.3 位段的跨平台问题
位段有严重的跨平台问题,使用时需要特别注意:
-
int位段被当成有符号数还是无符号数是不确定的
cstruct S { int a : 5; // a可能是有符号也可能是无符号 }; -
位段中最大位的数目不能确定
- 16位机器最大16,32位机器最大32
- 写成27,在16位机器会出问题
cstruct S { int a : 27; // 在16位机器上可能出错 }; -
位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义
cstruct S { unsigned int a : 1; unsigned int b : 2; unsigned int c : 3; }; // 内存布局可能是: [a][b][c] 或 [c][b][a] -
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
cstruct S { unsigned int a : 10; unsigned int b : 20; // 可能从新单元开始,也可能接着a后面 };
4.4 位段的应用
位段主要用于节省存储空间,常见应用场景:
- 网络协议头:IP、TCP、UDP等协议头中的标志位
- 硬件寄存器:嵌入式系统中访问硬件寄存器
- 状态标志:多个布尔标志打包存储
- 数据压缩:存储大量小范围数值
示例:IP协议头
c
struct IPHeader {
unsigned int version : 4; // IP版本号
unsigned int ihl : 4; // 头部长度
unsigned int tos : 8; // 服务类型
unsigned int total_length : 16; // 总长度
// ... 其他字段
};
4.5 位段使用的注意事项
-
不能取位段成员的地址
cstruct S { int a : 5; } s; // int* p = &s.a; // 错误:不能取位段成员的地址 -
位段成员不能是数组
c// struct S { // int arr[3] : 10; // 错误:位段不能是数组 // }; -
位段的赋值范围受位数限制
cstruct S { unsigned int a : 3; // 只能存储0-7 } s; s.a = 10; // 实际存储的是10 & 0x7 = 2 -
不同编译器实现可能不同
- 编写可移植代码时应避免使用位段
- 如果必须使用,要添加详细的平台说明
5. 结构体实现位段详解
5.1 位段的基本实现
c
#include <stdio.h>
#include <stdint.h>
// 使用结构体位段实现一个RGB颜色(24位)
struct RGBColor {
uint32_t red : 8; // 红色分量,8位
uint32_t green : 8; // 绿色分量,8位
uint32_t blue : 8; // 蓝色分量,8位
uint32_t alpha : 8; // 透明度,8位(可选)
};
// 使用结构体位段实现权限控制
struct FilePermissions {
unsigned int owner_read : 1;
unsigned int owner_write : 1;
unsigned int owner_exec : 1;
unsigned int group_read : 1;
unsigned int group_write : 1;
unsigned int group_exec : 1;
unsigned int other_read : 1;
unsigned int other_write : 1;
unsigned int other_exec : 1;
unsigned int setuid : 1;
unsigned int setgid : 1;
unsigned int sticky : 1;
unsigned int reserved : 4; // 保留位
};
int main() {
// RGB颜色示例
struct RGBColor color;
color.red = 255;
color.green = 128;
color.blue = 64;
color.alpha = 255;
printf("RGB颜色: #%02X%02X%02X\n",
color.red, color.green, color.blue);
printf("结构体大小: %zu 字节\n", sizeof(struct RGBColor));
// 文件权限示例
struct FilePermissions perm;
perm.owner_read = 1;
perm.owner_write = 1;
perm.owner_exec = 1;
perm.group_read = 1;
perm.group_write = 0;
perm.group_exec = 1;
perm.other_read = 1;
perm.other_write = 0;
perm.other_exec = 0;
perm.setuid = 0;
perm.s