当一堆相关数据想「住在一起」——比如一个学生要有姓名、年龄、分数——把它们打包放进同一座小房子里,就会舒服很多。这座小房子,就叫结构体(struct)。

房子的初样:声明与自引用

struct Stu {
    char name[20];
    int age;
    float score;
};

// 自引用:链表节点指向下一个同类节点
struct Node {
    int data;
    struct Node* next;   // 一定是"指针",不能是 struct Node next
};

还有「匿名结构体」——不写名字,用完即弃,之后就不能再用这个类型创建新变量了。

内存对齐:房子也要"工整"

看一个有点反直觉的现象:

struct S1 { char c1; char c2; int n; };   // 8 字节
struct S2 { char c1; int n;  char c2; };   // 12 字节 —— 同样的成员,顺序不同大小不同!

原因就是内存对齐。规则有四条:

  1. 第一个成员放在偏移量为 0 处。
  2. 其他成员要对齐到「对齐数」的整数倍地址;对齐数 = 编译器默认值(VS 为 8)与该成员大小的较小值。
  3. 结构体总大小 = 最大对齐数的整数倍。
  4. 嵌套结构体要对齐到自己成员中最大对齐数的整数倍。

以 S1 为例:c1 放 0,c2 放 1,n(对齐数 4)放 4~7,总共 8 字节。而 S2 里 n 被迫从 4 开始,c2 落在 8,总共 9,再补齐到最大对齐数 4 的倍数 → 12。

为什么要对齐?用空间换时间

一是平台原因:有些硬件只能在特定地址存取特定类型,否则会抛异常。二是性能原因:对齐的访问只要一次内存操作,未对齐可能要两次。所以内存对齐本质是拿一点点空间换更快的速度。

想省空间也有小技巧:让占用小的成员尽量挤在一起(就像 S1 那样)。还可用 #pragma pack(1) 修改默认对齐数(一般设成 2 的次方)。

传参:把地址交出去

void print1(struct S s);   // 传整个结构体:要压栈拷贝,太慢
void print2(struct S* ps); // 传地址:轻快 ✅

结构体过大时,传值会带来沉重的拷贝开销,所以结构体传参首选传地址。

位段:按"位"精打细算

struct A {
    int _a : 2;   // 只占 2 个比特位
    int _b : 5;
    int _c : 10;
    int _d : 30;
};   // sizeof = 8(而普通 int×4 是 16)

位段能狠狠节省空间,但它不跨平台——成员从哪边分配、超出剩余位时舍弃还是利用,标准并未定义。而且位段成员没有地址,不能用 & 取地址,也就不能直接 scanf。

结构体小房子的可爱插画
把相关的数据整整齐齐,收进同一座小房子

✿ 摘要

  • 结构体把相关数据打包成一种新类型,可自引用来实现链表。
  • 内存对齐:成员按「对齐数」整数倍摆放,全部大小凑成最大对齐数的倍数。
  • 对齐是为了性能与平台兼容,本质用空间换时间;小成员放一起更省空间。
  • 结构体传参优先传地址,避免大拷贝。
  • 位段按 bit 省空间,但不跨平台、不能取地址。

愿当下的花如你所见般美丽