| 目录 |

8 结构体

在 C 中,我们有一种叫作 struct 的东西,它是一种用户可定义的类型,可以容纳多份数据,而这些数据的类型可以各不相同。

这是把多个变量打包成一个变量的便捷方式。这样做有利于向函数传递变量(只需传一个,而不是传许多个),也有助于组织数据、让代码更易读。

如果你来自其他语言,可能熟悉类和对象的概念。C 原生并不支持这些68。你可以把 struct 看作一个只有数据成员、没有方法的类。

8.1 声明结构体

你可以像这样在代码中声明一个 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 里的字段赋值,然后在接下来的一小段里把这些值打印出来。

8.2 结构体初始化器

上一节的示例有点笨拙。初始化那个 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,我把它省略了)。

8.3 把结构体传给函数

要把 struct 传给函数,你有几种做法。

  1. 传 struct 本身。
  2. 传指向 struct 的指针。

回想一下,当你把某个东西传给函数时,会为函数制作该东西的一份副本供其操作,不管那是一份指针、一个 int、一个 struct,还是别的什么。

基本上有两种情况,你会想传指向 struct 的指针:

  1. 你需要让函数能够修改传入的那个 struct,并且让这些修改在调用方可见。
  2. 该 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 有一种语法糖,叫作箭头运算符,能帮忙解决这个问题。

8.4 箭头运算符

箭头运算符可以帮助引用指向 struct 的指针中的字段。

void set_price(struct car *c, float new_price) {
    // (*c).price = new_price;  // 能用,但不是惯用写法 :(
    //
    // 上面那行与下面那行 100% 等价:

    c->price = new_price;  // 就是这个!
}

那么,访问字段时,什么时候用点,什么时候用箭头呢?

8.5 复制与返回 struct

这个很简单!

直接从一个赋值给另一个就行!

struct car a, b;

b = a;  // 复制结构体

从函数返回 struct(而不是指向它的指针)时,也会向接收变量做一次类似的复制。

这并不是“深拷贝”71。所有字段都按原样复制,包括指向其他东西的指针。

8.6 比较 struct

只有一种安全的做法:逐个比较每个字段。

你可能以为可以用 memcmp()72,但它处理不了其中可能存在的填充字节。

如果你先用 memset()73 把 struct 清零,那它也许能行,不过其中可能有某些奇怪的元素,它们的比较结果可能和你想的不一样74。


| 目录 |