在 C 中,我们有一种叫作 struct 的东西,它是一种用户可定义的类型,可以容纳多份数据,而这些数据的类型可以各不相同。
这是把多个变量打包成一个变量的便捷方式。这样做有利于向函数传递变量(只需传一个,而不是传许多个),也有助于组织数据、让代码更易读。
如果你来自其他语言,可能熟悉类和对象的概念。C 原生并不支持这些68。你可以把 struct 看作一个只有数据成员、没有方法的类。
你可以像这样在代码中声明一个 struct:
struct car {
char *name;
float price;
int speed;
};这通常在任何函数之外的全局作用域中完成,这样该 struct 就能全局使用。
这样做的时候,你是在创建一个新的类型。完整的类型名是 struct car。(不能只写 car——那样不行。)
此时还没有任何该类型的变量,但我们可以声明一些:
struct car saturn; // 类型为 "struct car" 的变量 "saturn"现在我们就有了一个类型为 struct car 的未初始化变量 saturn69。
我们应该初始化它!可怎么给那些单独的字段设置值呢?
就像许多从 C 那里偷走它的其他语言一样,我们要用点运算符(.)来访问各个字段。
saturn.name = "Saturn SL/2";
saturn.price = 15999.99;
saturn.speed = 175;
printf("Name: %s\n", saturn.name);
printf("Price (USD): %f\n", saturn.price);
printf("Top Speed (km): %d\n", saturn.speed);在那里,开头几行我们给 struct car 里的字段赋值,然后在接下来的一小段里把这些值打印出来。
上一节的示例有点笨拙。初始化那个 struct 变量一定有更好的办法!
你可以在定义变量时用初始化器,按字段在 struct 中出现的顺序为它们填入值。(这在变量定义之后就不行了——必须写在定义里。)
struct car {
char *name;
float price;
int speed;
};
// 现在有了初始化器!字段顺序与 struct 声明中的相同:
struct car saturn = {"Saturn SL/2", 16000.99, 175};
printf("Name: %s\n", saturn.name);
printf("Price: %f\n", saturn.price);
printf("Top Speed: %d km\n", saturn.speed);初始化器里的字段必须保持相同顺序,这一点有点吓人。如果有人改动了 struct car 中的顺序,就可能破坏所有其他代码!
我们可以让初始化器更明确一些:
struct car saturn = {.speed=175, .name="Saturn SL/2"};现在它就不再依赖 struct 声明中的顺序了。这样代码肯定更安全。
与数组初始化器类似,任何缺失的字段指定初始化器都会被初始化为零(在这个例子里就是 .price,我把它省略了)。
要把 struct 传给函数,你有几种做法。
struct 本身。struct 的指针。回想一下,当你把某个东西传给函数时,会为函数制作该东西的一份副本供其操作,不管那是一份指针、一个 int、一个 struct,还是别的什么。
基本上有两种情况,你会想传指向 struct 的指针:
struct,并且让这些修改在调用方可见。struct 有点大,把它复制到栈上的开销比只复制一个指针更大70。出于这两个原因,把指向 struct 的指针传给函数要常见得多,不过传 struct 本身绝不违法。
我们来试试传指针:写一个函数,用来设置 struct car 的 .price 字段:
#include <stdio.h>
struct car {
char *name;
float price;
int speed;
};
int main(void)
{
struct car saturn = {.speed=175, .name="Saturn SL/2"};
// 把这个 struct car 的指针传进去,同时传一个新的、
// 更贴近现实的价格:
set_price(&saturn, 799.99);
printf("Price: %f\n", saturn.price);
}只看一下那里实参的类型,你就应该能写出 set_price() 的函数签名。
saturn 是一个 struct car,所以 &saturn 必定是这个 struct car 的地址,也就是指向 struct car 的指针,即 struct car*。
而 799.99 是一个 float。
所以函数声明必须长这样:
void set_price(struct car *c, float new_price)我们只需要写函数体。一种尝试可能是:
void set_price(struct car *c, float new_price) {
c.price = new_price; // 错误!!
}这样不行,因为点运算符只作用于 struct……它不能作用于指向 struct 的指针。
好吧,那我们可以解引用变量 c,去掉它的指针身份,得到 struct 本身。解引用一个 struct car* 得到的是该指针所指向的 struct car,对它应该就能用点运算符了:
void set_price(struct car *c, float new_price) {
(*c).price = new_price; // 能用,但难看,而且不是惯用写法 :(
}这样确实可行!但要把那些括号和星号全都打出来有点笨重。C 有一种语法糖,叫作箭头运算符,能帮忙解决这个问题。
箭头运算符可以帮助引用指向 struct 的指针中的字段。
void set_price(struct car *c, float new_price) {
// (*c).price = new_price; // 能用,但不是惯用写法 :(
//
// 上面那行与下面那行 100% 等价:
c->price = new_price; // 就是这个!
}那么,访问字段时,什么时候用点,什么时候用箭头呢?
struct,就用点(.)。struct 的指针,就用箭头(->)。 struct这个很简单!
直接从一个赋值给另一个就行!
struct car a, b;
b = a; // 复制结构体从函数返回 struct(而不是指向它的指针)时,也会向接收变量做一次类似的复制。
这并不是“深拷贝”71。所有字段都按原样复制,包括指向其他东西的指针。
struct只有一种安全的做法:逐个比较每个字段。
你可能以为可以用 memcmp()72,但它处理不了其中可能存在的填充字节。
如果你先用 memset()73 把 struct 清零,那它也许能行,不过其中可能有某些奇怪的元素,它们的比较结果可能和你想的不一样74。