C 语言结构类型简介

目录汇总:C 语言零基础入门教程

C 语言结构变量简介 虽然说明了声明结构变量的方法,但是没有讨论一个重要的问题:命名结构类型。假设程序需要声明几个具有相同成员的结构变量。如果一次可以声明全部变量,那么没有什么问题。但是,如果需要在程序中的不同位置声明变量,那么问题就复杂了。如果在某处编写了

1
2
3
4
5
struct {
  int number;
  char name[NAME_LEN+1];
  int on_hand;
} part1;

并且在另一处编写了

1
2
3
4
5
struct {
  int number;
  char name[NAME_LEN+1];
  int on_hand;
} part2;

那么立刻就会出现问题。重复的结构信息会使程序膨胀。因为难以确保这些声明会保持一致,所以将来修改程序会有风险。

但是这些还不是最大的问题。根据 C 语言的规则,part1part2 不具有兼容的类型,因此不能把 part1 赋值给 part2,反之亦然。而且,因为 part1part2 的类型都没有名字,所以也就不能把它们用作函数调用的参数。

为了克服这些困难,需要定义表示结构类型(而不是特定的结构变量)的名字。C 语言提供了两种命名结构的方法:可以声明“结构标记”,也可以使用 typedef 来定义类型名[类型定义(C 语言类型定义简介)]。

一、结构标记的声明

结构标记(structure tag)是用于标识某种特定结构的名字。下面的例子声明了名为 part 的结构标记:

1
2
3
4
5
struct part {
  int number;
  char name[NAME_LEN+1];
  int on_hand;
};

注意,右花括号后的分号是必不可少的,它表示声明结束。

注意

如果无意间忽略了结构声明结尾的分号,可能会导致奇怪的错误。考虑下面的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct part {
 int number;
 char name[NAME_LEN+1];
 int on_hand;
}              /*** WRONG: semicolon missing ***/

f(void)
{
 ...
 return 0;    /* error detected at this line */
}

程序员没有指定函数 f 的返回类型(编程有点儿随意)。因为前面的结构声明没有正常终止,所以编译器会假设函数 f 的返回值是 struct part 类型的。编译器直到执行函数中第一条 return 语句时才会发现错误,结果得到含义模糊的出错消息。

一旦创建了标记 part,就可以用它来声明变量了:

1
struct part part1, part2;

但是,不能通过省略单词 struct 来缩写这个声明:

1
part part1, part2;     /*** WRONG ***/

part 不是类型名。如果没有单词 struct 的话,它就没有任何意义。

因为结构标记只有在前面放置了单词 struct 时才会有意义,所以它们不会和程序中用到的其他名字发生冲突。程序拥有名为 part 的变量是完全合法的(虽然有点儿容易混淆)。

顺便说一句,结构标记的声明可以和结构变量的声明合并在一起:

1
2
3
4
5
struct part {
  int number;
  char name[NAME_LEN+1];
  int on_hand;
} part1, part2;

在这里不仅声明了结构标记 part(可能稍后会用 part 声明更多的变量),而且声明了变量 part1part2

所有声明为 struct part 类型的结构彼此之间是兼容的:

1
2
3
4
struct part part1 = {528, "Disk drive", 10};
struct part part2;

part2 = part1;    /* legal; both parts have the same type */

二、结构类型的定义

除了声明结构标记,还可以用 typedef 来定义真实的类型名。例如,可以按照如下方式定义名为 Part 的类型:

1
2
3
4
5
typedef struct  {
  int number;
  char name[NAME_LEN+1];
  int on_hand;
} Part;

注意,类型 Part 的名字必须出现在定义的末尾,而不是在单词 struct 的后边。

可以像内置类型那样使用 Part。例如,可以用它声明变量:

1
Part part1, part2;

因为类型 Parttypedef 的名字,所以不允许书写 struct Part。无论在哪里声明,所有的 Part 类型的变量都是兼容的。

需要命名结构时,通常既可以选择声明结构标记,也可以使用 typedef。但是,正如稍后将看到的,结构用于链表时,强制使用声明结构标记。在本 C 语言零基础入门教程 的大多数例子中,我使用的是结构标记而不是 typedef 名。

三、结构作为参数和返回值

函数可以有结构类型的实际参数和返回值。下面来看两个例子。当把 part 结构用作实际参数时,第一个函数显示出结构的成员:

1
2
3
4
5
6
void print_part(struct part p)
{
  printf("Part number: %d\n", p.number);
  printf("Part name: %s\n", p.name);
  printf("Quantity on hand: %d\n", p.on_hand);
}

下面是 print_part 可能的调用方法:

1
print_part(part1);

第二个函数返回 part 结构,此结构由函数的实际参数构成:

1
2
3
4
5
6
7
8
9
struct part build_part(int number, const char * name, int on_hand)
{
  struct part p;

  p.number = number;
  strcpy (p.name, name);
  p.on_hand = on_hand;
  return p;
}

注意,函数 build_part 的形式参数名和结构 part 的成员名相同是合法的,因为结构拥有自己的名字空间。下面是 build_part 可能的调用方法:

1
part1 = build_part(528, "Disk drive", 10);

给函数传递结构和从函数返回结构都要求生成结构中所有成员的副本。这样的结果是,这些操作对程序强加了一定数量的系统开销,特别是结构很大的时候。为了避免这类系统开销,有时用传递指向结构的指针来代替传递结构本身是很明智的做法。类似地,可以使函数返回指向结构的指针来代替返回实际的结构。

除了效率方面的考虑之外,避免创建结构的副本还有其他原因。例如,<stdio.h> 定义了一个名为 FILE 的类型,它通常是结构。每个 FILE 结构存储的都是已打开文件的状态信息,因此在程序中必须是唯一的。<stdio.h> 中每个用于打开文件的函数都返回一个指向 FILE 结构的指针,每个对已打开文件执行操作的函数都需要用 FILE 指针作为参数。

有时,可能希望在函数内部初始化结构变量来匹配其他结构(可能作为函数的形式参数)。在下面的例子中,part2 的初始化器是传递给函数 f 的形式参数:

1
2
3
4
5
void f(struct part part1)
{
  struct part part2 = part1;
  ...
}

C 语言允许这类初始化器,因为初始化的结构(此例中的 part2)具有自动存储期(C 语言局部变量简介),也就是说它局部于函数并且没有声明为 static。初始化器可以是适当类型的任意表达式,包括返回结构的函数调用。

四、复合字面量

C 语言函数中的形式参数和实际参数 介绍过从 C99 开始引入的新特性复合字面量。在那一篇中,复合字面量被用于创建没有名字的数组,这样做的目的通常是将数组作为参数传递给函数。复合字面量同样也可以用于“实时”创建一个结构,而不需要先将其存储在变量中。生成的结构可以像参数一样传递,可以被函数返回,也可以赋值给变量。接下来看两个例子。

首先,使用复合字面量创建一个结构,这个结构将传递给函数。例如,可以按如下方式调用 print_part 函数:

print_part((struct part) {528, "Disk drive", 10});

上面的复合字面量(用加粗字体表示)创建了一个 part 结构,依次包括成员 528"Disk drive"10。这个结构之后被传递到 print_part 显示。

下面的语句把复合字面量赋值给变量:

1
part1 = (struct part) {528, "Disk drive", 10};

这一语句类似于包含初始化器的声明,但不完全一样——初始化器只能出现在声明中,不能出现在这样的赋值语句中。

一般来说,复合字面量包括用圆括号括住的类型名和后续的初始化器。如果复合字面量代表一个结构,类型名可以是结构标签的前面加上 struct(如本例所示)或者 typedef 名。一个复合字面量的初始化器部分可以包含指示器:

1
2
3
print_part((struct part) {.on_hand = 10,
                          .name = "Disk drive",
                          .number = 528});

复合字面量不会提供完全的初始化,所以任何未初始化的成员默认值为 0。

五、匿名结构

从 C11 开始,结构或者联合的成员也可以是另一个没有名字的结构。如果一个结构或者联合包含了这样的成员:

(1) 没有名称;

(2) 被声明为结构类型,但是只有成员列表而没有标记。

则这个成员就是一个匿名结构(anonymous structure)。

在下例中,struct tunion u 的第二个成员都是匿名结构。

1
2
struct t {int i; struct {char c; float f;};};
union u {int i; struct {char c; float f;};};

现在的问题是,如何才能访问匿名结构的成员?若某个匿名结构 S 是结构或者联合 X 的成员,那么 S 的成员就被当作 X 的成员。进一步,对于多层嵌套的情况,如果符合以上条件,则可以递归地应用这种关系。

在下面的例子中,struct t 包含了一个没有标记、没有名称的结构成员,这个结构成员的成员 cf 被认为属于 struct t

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct t
{
  int i;
  struct s {int j, k:3;};          // 有标记的成员
  struct {char c; float f;};       // 无标记且未命名的成员
  struct {double d;} s;            // 命名的成员
} t;

t.i = 2006;
t.j = 5;                           // 非法
t.k = 6;                           // 非法
t.c = 'x';                         // 正确
t.f = 2.0;                         // 正确
t.s.d = 22.2;

出于同样的原因,下面的类型声明将在转换期间得到一个表示错误的诊断信息。因为 struct tag 的第二个成员是匿名结构,而匿名结构的成员中又有一个是匿名结构,所以,匿名结构的成员 if 被当作 struct tag 的成员,这意味着 struct tag 有两个成员的名称相同,都是 i

1
2
3
4
5
6
struct tag
{
  struct {int i;};
  struct {struct {int i; float f;}; double d;};
  char c;
};

尽管匿名结构的成员被当作隶属于包含该结构的上层结构的成员,但它的初始化器依然必须采用被花括号包围的形式。

在下例中,尽管匿名结构的成员 x 被认为属于包含它的那个结构 struct t,但它的初始化器仍然需要使用一对花括号。

1
2
3
struct t {char c; struct {int x;};};
struct t t = {'x', 1};         // 非法
struct t t = {'x', {1}};       // 合法

请参阅

(完)

comments powered by Disqus