写C代码的什意思人迟早会撞上struct。它不复杂,什意思但教科书的什意思讲法常常让人绕远路。先看一段真实的什意思代码:
struct Point { int x; int y; }; struct Point p1; p1.x = 3; p1.y = 7; 这就是struct最朴素的样子。它把几个变量捆在一起,什意思给这捆东西起个名字叫Point。什意思之后你可以用p1.x、什意思p1.y来访问里面的什意思成员。没有魔法,什意思没有运行时开销,什意思编译器只是什意思帮你算好偏移量——x在地址+0的位置,y在地址+4的什意思位置(假设int是4字节)。
很多人第一次学struct的什意思时候会问:这跟数组有什么区别?数组要求所有元素类型相同,struct不要求。什意思数组用下标访问,什意思struct用名字访问。更关键的是,数组的语义是“一堆同样的东西”,struct的语义是“一个东西的几个组成部分”。Point是一个点,它有x和y两个坐标,而不是“两个int组成的列表”。这个区别听起来像文字游戏,但它决定了你什么时候该用哪个。
C语言里的struct有个让初学者困惑的地方:声明和定义是分开的。
struct Point; // 声明,告诉编译器有这么个东西 struct Point { int x; int y; }; // 定义,告诉编译器它长什么样 你可以先声明一个struct,在别的地方再定义它。这跟函数的前向声明是一个道理。实际写代码的时候,struct的定义通常放在头文件里,因为多个.c文件都需要知道它的布局。
还有一个经典坑:typedef。
typedef struct { int x; int y; } Point; 这样写之后,你可以直接用Point p1;,不用每次写struct Point p1;。C++里struct默认就带了这个效果,所以C++程序员转回C的时候经常忘记加struct关键字,然后被编译器骂。
内存对齐是struct里最容易被忽视的细节。看这个:
struct A { char c; int i; }; sizeof(struct A)在大多数平台上不是5,而是8。因为编译器会在c后面塞3个字节的填充,让i的地址对齐到4的倍数。你可以用#pragma pack或者__attribute__((packed))来关掉对齐,但那样访问速度会变慢,某些架构上甚至直接崩溃。我见过有人写网络协议解析的时候忘了这回事,结构体发出去对方收到的字节全错位了。
struct可以嵌套。一个struct的成员可以是另一个struct:
struct Rectangle { struct Point topLeft; struct Point bottomRight; }; 访问的时候就是rect.topLeft.x。层级深了会很难看,所以C程序员经常用指针来简化——rect->topLeft.x,或者干脆把常用的字段提出来。
指针和struct搭配是C里最常见的组合。链表、树、图,这些数据结构全靠struct里放一个指向自身类型的指针来实现:
struct Node { int value; struct Node *next; }; 注意这里struct Node *next是合法的,虽然Node还没定义完。编译器只需要知道struct Node是个类型名,指针的大小是固定的,具体布局可以稍后再说。这是C语言设计里比较聪明的一处。
位域是struct的另一个用法,虽然日常写业务代码不太碰:
struct Flags { unsigned int a : 1; unsigned int b : 2; unsigned int c : 5; }; 冒号后面的数字表示这个成员占几个bit。整个struct可能只占一个字节。嵌入式开发里读写硬件寄存器的时候经常这么干。但位域的布局依赖编译器和平台,跨平台代码里最好别用。
最后说一个实际观察:很多C项目里struct的定义位置很讲究。如果某个struct只在单个.c文件里用,就放在那个文件的顶部。如果多个文件共享,就放头文件。头文件里的struct定义要尽量小,因为每个包含它的.c文件都会看到完整定义。有人喜欢用不透明指针——头文件里只写typedef struct Foo Foo;,具体定义藏在.c里。这样修改struct的成员不需要重新编译所有依赖它的文件。代价是别人不能直接访问成员,得通过函数。值不值得,看项目规模。
struct说到底就是内存布局的描述。你告诉编译器“这块内存里,前4个字节是x,后4个字节是y”,编译器就按这个规则帮你生成访问代码。没有继承,没有虚函数,没有访问控制。简单,但够用。