1. 结构体的语法基础:先把代码跑起来再谈原理
1.1 为什么需要结构体:从散装变量到打包数据
写程序久了你会发现,单纯靠一堆单个变量组织数据,很快就会失控。就拿一个学生信息做例子:学号、姓名、成绩、班级,如果你用四个独立的变量去存,在这段代码里是 id、name、score、class_num,到了下一段代码可能就变成 a、b、c、d,传参的时候四个变量一个个传,调用多了连自己都分不清谁是谁。更麻烦的是,如果你需要记录 50 个学生的信息,就得定义 200 个变量,这代码基本没法维护。
这就是结构体存在的核心意义:它把逻辑上相关的数据打包成一个整体,让编译器帮我们管理数据的组织关系。结构体的英文叫 struct,是 "structure" 的缩写,在 C 语言里它是自定义类型的基石,到了 C++、Go、Rust 里都有对应的概念,Python 里的 dataclass 本质上也是同一件事。可以说,只要你想把多个属性绑定在一起作为一个独立单元去操作,就离不开结构体。
这篇内容我会从结构体的语法开始,逐步深入到它在内存中的真实布局——也就是标题里说的“藏与露”。所谓“露”,是你用 . 或 -> 能直接访问到的成员;所谓“藏”,是结构体在内存里为了对齐和访问效率偷偷塞进去的那些填充字节,以及堆上动态分配时那些掌控权和风险。这篇文章适合刚学结构体、以及会用了但没想过内存布局的读者,看完能少踩很多坑。
1.2 定义、初始化和访问:三种定义方式的区别
先看最基本的定义方式,C 语言里最常见的写法是这样的:
c复制#include <stdio.h>
// 方式一:先定义类型,再定义变量
struct Student {
int id;
char name[32];
float score;
};
// 方式二:定义类型的同时定义变量
struct Point {
int x;
int y;
} p1, p2;
// 方式三:使用 typedef 别名
typedef struct {
int year;
int month;
int day;
} Date;
int main() {
// 初始化
struct Student stu1 = {1001, "张三", 89.5f};
struct Student stu2;
stu2.id = 1002;
snprintf(stu2.name, sizeof(stu2.name), "李四");
stu2.score = 76.0f;
// 访问成员
printf("stu1: %d %s %.1f\n", stu1.id, stu1.name, stu1.score);
printf("stu2: %d %s %.1f\n", stu2.id, stu2.name, stu2.score);
printf("p1: (%d, %d)\n", p1.x, p1.y);
printf("date: %d-%d-%d\n", (Date){2024, 5, 1}.year, 5, 1);
return 0;
}
注意:方式二里
p1、p2是变量,struct Point是类型。如果你只写了struct Point { ... };后面没有变量名,那只是声明了一个类型,并没有创建变量。这个很多初学者会混淆,记住“结构体定义是造图纸,变量定义才是真正造房子”。
关于初始化,C99 标准支持指定初始化器,这种写法在实际项目里非常实用,因为你不必记住字段的顺序,也不怕以后在结构体中间插入新字段导致原有初始化崩掉:
c复制struct Student stu3 = {
.id = 1003,
.name = "王五",
.score = 95.5f
};
C++ 里更是可以直接这么初始化,语法一样。这里有个很容易踩的坑:如果你用了指定初始化器,却又漏掉了中间某个字段,那么漏掉的字段会被自动置为 0。这在有些场景下是好事,但在某些场景下你可能根本没意识到某个字段变成了 0,从而引入难以排查的逻辑 bug。
访问成员时,普通变量用 .、指针变量用 ->,这是最基础也最重要的规则。如果你有一个结构体指针 p,访问它的成员有两种等价写法:(*p).id 和 p->id。大多数人都直接写 ->,因为简洁明了。但你要明白这两者在底层是完全一样的,-> 只是语法糖——本质上都是先拿到结构体的起始地址,再加上成员相对起始地址的偏移量,定位到那个成员的内存位置。
1.3 结构体数组与嵌套:组织批量数据
单个结构体能管一条数据,结构体数组就能管一批同类型的数据。50 个学生的信息,一个数组就搞定了:
c复制struct Student class1[50];
class1[0].id = 2001;
snprintf(class1[0].name, sizeof(class1[0].name), "赵六");
这个数组在内存里是连续存放的,每个元素占用 sizeof(struct Student) 个字节。这意味着你可以用指针偏移的方式遍历,也可以用下标遍历,本质上是一回事。这里有一个性能上的细节:结构体数组因为内存连续,所以对 CPU 缓存非常友好,遍历的时候命中率很高。相比之下,如果你用一个指针数组,每个指针指向堆上单独分配的结构体,遍历时就要在内存地址之间跳来跳去,缓存命中率就差了。性能敏感的程序里,优先考虑结构体数组。
结构体嵌套也很常见,比如学生信息里包含生日(Date),地址(Address)等。嵌套的初始化也不难:
c复制typedef struct {
int year;
int month;
int day;
} Date;
typedef struct {
int id;
char name[32];
Date birthday; // 嵌套结构体
} Student;
Student stu = {3001, "钱七", {2000, 5, 20}};
嵌套结构体在内存中就是简单地“展开”,子结构体的成员按它的布局规则被嵌入到父结构体中,完全不存在什么“引用”或“指针跳转”的机制。这一点很多人没意识到,他们在脑子里把结构体想象得过于复杂,其实结构体就是一段连续内存的“地图”,每个成员按顺序(和对齐规则)排布在这段内存上。
