C语言·结构体
从为什么需要结构体到内存对齐,一次讲透
一、为什么需要结构体
假设你要管理一个学生信息系统。每个学生有姓名、学号、年龄、成绩。用单独的数组?
char names[100][32];
int ids[100];
int ages[100];
float scores[100];
这样写,几个问题:一是同一个学生的数据被拆散在四个数组里,逻辑上不内聚;二是排序时要同步移动四个数据,繁琐;三是参数传递时要传四个指针,冗长。
你想要的是一个能把属性绑在一起的容器——这就是结构体。
二、结构体的基本语法
1. 定义
struct Student {
char name[32];
int id;
int age;
float score;
};
结构体可以看作是你自己定义的新类型。struct 是关键字,Student 是你给这个类型起的名字。花括号里是成员列表。
2. 声明变量
struct Student s1; //直接声明
struct Student s2 = {"张三", 1001, 18, 92.5}; //初始化
typedef struct Student Student; //别名
Student s3; //省略struct
每次写 struct Student 太麻烦,第三行用 typedef 给它起了个简称 Student。也可以在定义时直接写:
typedef struct {
int x, y;
} Point;
3. 访问成员
s1.age = 20; //点操作符
printf("%s", s2.name); //读取
strcpy(s1.name, "李四"); //字符串成员要用strcpy
结构体变量用点操作符 "." 访问成员。如果是指针,则用箭头 "->"。
三、结构体与指针
1. 指针访问结构体
Student s = {"王五", 1002, 19, 88.0};
Student *p = &s;
p->age = 20; //等价(*p).age
p->age 是 (*p).age 的语法糖。为什么要用括号?因为点的优先级比星号高——*p.age 会被解释为 *(p.age)。
2. 动态分配结构体
Student *p = (Student*)malloc(sizeof(Student));
if (p == NULL) { /*分配失败*/ }
strcpy(p->name, "赵六");
p->id = 1003;
free(p);
malloc 返回 void*,必须强制转换为 Student*。用完记得 free。
四、结构体数组
结构体数组是结构体最常见的用法之一:
Student class[50];
for (int i = 0; i < 50; i++) {
scanf("%s %d %d %f", class[i].name,
&class[i].id, &class[i].age, &class[i].score);
}
结构体数组排序用 qsort:
int cmp(const void *a, const void *b) {
return ((Student*)b)->score - ((Student*)a)->score;
}
qsort(class, 50, sizeof(Student), cmp);
qsort 是标准库函数,需要你提供比较函数。不要自己写排序——qsort 已经帮你做好了。
五、内存对齐
结构体成员在内存中并不是紧密排列的。编译器会插入填充字节以满足对齐要求。
struct Demo {
char a; // 1字节
int b; // 4字节
char c; // 1字节
};
// sizeof(struct Demo) = 12,不是6
原因:编译器按最大成员对齐——int 要4字节对齐,所以 a 后面补 3 字节,c 后面补 3 字节。
解决方案:把大的成员放前面:
struct Demo2 {
int b; // 4字节-> sizeof=8
char a, c; // 1+1,后面补2
};
这叫结构体对齐,知道就行,不要迷信 sizeof。
六、嵌套结构体
结构体可以包含另一个结构体:
typedef struct {
int x, y;
} Point;
typedef struct {
Point start, end;
} Line;
Line l = {{0,0}, {10,10}};
printf("%d", l.start.x); //访问嵌套成员
但是不能包含自己类型的实例(无限递归大小)。可以包含自己类型的指针:
typedef struct Node {
int data;
struct Node *next; //指针,大小确定
} Node;
这就是链表的基础。
七、位段与联合体
1. 位段:省内存
struct Flags {
unsigned int is_on : 1; //只占1位
unsigned int mode : 3; //只占3位
unsigned int : 0; //强制对齐
};
位段在嵌入式开发中很常见,用来表示寄存器中每个 bit 的含义。
2. 联合体:共享内存
union Data {
int i;
float f;
char s[4];
};
union Data d;
d.i = 0x12345678;
// d.f和d.s现在被覆盖了
联合体的所有成员共享同一块内存。sizeof(union) = 最大成员的大小。用途:节省内存、类型转换、判断大小端。
八、结构体传参
结构体作为函数参数时有三种方式:
void f1(Student s); //值传递,拷贝整个结构体,很慢
void f2(Student *p); //指针传递,只传8字节
void f3(const Student *p); //只读指针,不能修改
原则:用指针传递,能用 const 就用 const。结构体可能很大,值传递会在栈上拷贝整个结构体,浪费内存和时间。
"Toto,I've a feeling we're not in Kansas anymore."
—— The Wizard of Oz