11.2 再探结构成员
前面说过,所有基本数据类型(包含数组)都可以成为结构的成员。除此之外,还可以把一个结构作为另一个结构的成员,不仅指针可以是结构的成员,结构指针也可以是结构的成员。
使用结构为编程打开了一个全新领域的大门,同时也增加了潜在的危机。下面逐一探讨这些内容,深入了解结构成员的组成。
11.2.1 将一个结构作为另一个结构的成员
本章的开头为满足马饲养员的需要,设计了一个程序,处理每匹马的各种数据,包括名字、身高和生日等,但程序11.1用年龄代替了生日。其部分原因是日期处理起来比较麻烦,要用3个数值表示,还要处理闰年的问题。现在准备将一个结构作为另一个结构的成员来处理日期。
可以定义一个用于保存日期的结构类型。下面的语句用标记符名称Date定义了这个结构:
struct Date
{
int day;
int month;
int year;
};
现在定义结构horse,其中包含出生日期变量,如下所示:
struct horse
{
struct Date dob;
int height;
char name[20];
char father[20];
char mother[20];
};
现在结构中有一个变量成员,它代表马的出生日期的结构。接下来用通常的语句定义一个horse结构的实例,如下所示:
struct horse Dobbin;
用与前面相同的语句为成员height设定值:
Dobbin.height = 14;
要在一系列赋值语句中设定出生日期,可以使用下面的逻辑:
Dobbin.dob.day = 5;
Dobbin.dob.month = 12;
Dobbin.dob.year = 1962;
这是一匹很老的马,表达式Dobbin.dob.day引用了int类型的变量,所以可以将它用于算术表达式或比较表达式。但如果使用Dobbin.dob,就会引用一个Date类型的结构变量。Date不是一个基本类型,而是一个结构,所以只能使用下面的方式赋值:
Trigger.dob = Dobbin.dob;
这行语句表示两匹马是双胞胎,但不能保证事实如此。
可以将第一个结构用作第二个结构的成员,再将第二个结构作为第三个结构的成员,依此类推。但C编译器只允许结构最多有15层。如果结构有这么多层,则引用最底层的成员时,需要输入所有的结构成员名称。
11.2.2 声明结构中的结构
可以在horse结构的定义中声明Date结构,如下:
struct horse
{
struct Date
{
int day;
int month;
int year;
} dob;
int height;
char name[20];
char father[20];
char mother[20];
};
这个声明将Date结构声明放在horse结构的定义内,因此不能在horse结构的外部声明Date变量。当然,每个horse类型的变量都包含Date类型的成员dob。但下面的语句:
struct Date my_date;
会导致编译错误。错误信息会说明Date结构类型未定义。如果需要在horse结构的外部使用Date,就必须将它定义在horse结构之外。
11.2.3 将结构指针用作结构成员
任何指针都可以是结构的成员,包含结构指针在内。结构成员指针可以指向相同类型的结构。例如,horse类型的结构可以含有一个指向horse类型结构的指针。
试试看:将结构指针用作结构成员
修改前一个例子,让结构含有指向同类型结构的指针:
/* Program 11.4 Daisy chaining the horses */
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
int main(void)
{
struct horse /* Structure declaration */
{
int age;
int height;
char name[20];
char father[20];
char mother[20];
struct horse *next; /* Pointer to next structure */
};
struct horse *first = NULL; /* Pointer to first horse */
struct horse *current = NULL; /* Pointer to current horse */
struct horse *previous = NULL; /* Pointer to previous horse */
char test = '\0'; /* Test value for ending input */
for( ; ; )
{
printf("\nDo you want to enter details of a%s horse (Y or N)? ",
first != NULL?"nother " : "" );
scanf(" %c", &test );
if(tolower(test) == 'n')
break;
/* Allocate memory for a structure */
current = (struct horse*) malloc(sizeof(struct horse));
if(first == NULL)
first = current; /* Set pointer to first horse */
if(previous != NULL)
previous -> next = current; /* Set next pointer for previous horse */
printf("\nEnter the name of the horse: ");
scanf("%s", current -> name); /* Read the horse's name */
printf("\nHow old is %s? ", current -> name);
scanf("%d", ¤t -> age); /* Read the horse's age */
printf("\nHow high is %s ( in hands )? ", current -> name );
scanf("%d", ¤t -> height); /* Read the horse's height */
printf("\nWho is %s's father? ", current -> name);
scanf("%s", current -> father); /* Get the father's name */
printf("\nWho is %s's mother? ", current -> name);
scanf("%s", current -> mother); /* Get the mother's name */
current->next = NULL; /* In case it's the last... */
previous = current; /* Save address of last horse */
}
/* Now tell them what we know. */
current = first; /* Start at the beginning */
while (current != NULL) /* As long as we have a valid pointer */
{ /* Output the data*/
printf("\n\n%s is %d years old, %d hands high,",
current->name, current->age, current->height);
printf(" and has %s and %s as parents.", current->father,
current->mother);
previous = current; /* Save the pointer so we can free memory */
current = current->next; /* Get the pointer to the next */
free(previous); /* Free memory for the old one */
}
return 0;
}
如果输入相同,这个例子会产生和程序11.3相同的输出。
代码的说明
这次不但没有为结构分配空间,而且只定义了三个指针。这些指针用下面的语句声明和初始化:
struct horse *first = NULL; /* Pointer to first horse */
struct horse *current = NULL; /* Pointer to current horse */
struct horse *previous = NULL; /* Pointer to previous horse */
每个指针都定义成horse结构的指针。first指针仅用于存储第一个结构的地址。第二和第三个指针是工作用的存储器:current存储了正在处理的horse结构的地址,previous跟踪前一个处理过的结构的地址。
horse结构中新增的next成员是指向horse结构的指针。每个horse结构中的next都指向下一个horse的地址,以链接所有的horse结构。但最后一个horse结构例外,它的next设定成NULL。这个结构的其他方面与前面相同,如图11-3所示。

图11-3 链接起来的horse结构
输入循环如下:
for( ; ; )
{
...
}
因为没有使用数组,不需要考虑索引,所以输入循环是一个无限循环。也不需要计算读入了多少组数据,所以不需要使用变量hcount及循环变量i。因为给每个horse结构分配了内存,所以只需接受输入的数据。
循环中的开始语句如下:
printf("\nDo you want to enter details of a%s horse (Y or N)? ",
first != NULL?"nother " : "" );
scanf(" %c", &test );
if(tolower(test) == 'n')
break;
在提示后,如果回答是N或n,就结束循环。否则,就准备接受另一组结构成员。first指针只有在第一次迭代时是NULL,所以在第二次以后的迭代中,其提示信息会和第一次稍有不同。
回答了循环开头的问题后,就执行下面的语句:
current = (struct horse*) malloc(sizeof(struct horse));
if(first == NULL)
first = current; /* Set pointer to first horse */
if(previous != NULL)
previous -> next = current; /* Set next pointer for previous horse */
每次迭代时,都为当前的结构分配必要的内存。为了精简程序,没有检查malloc()函数是否返回了NULL,但在实际使用时应检查。
如果指针first等于NULL,就表示是第一次迭代,即开始输入第一个结构。因此,将first指针设置为malloc()函数返回的指针值,即current变量存储的值。first中的地址也是访问链中第一个horse结构的关键。可以从first中的地址开始,利用成员next指针得到下一个结构的地址,再依序访问下一个结构,从而到达任何一个horse结构。
如果有下一个结构,就必须将next指针指向这个结构,但只要有下一个结构,就可以确定其地址。因此,在第二次和后续的迭代中,应将当前结构的地址存储到前一个结构的next成员中,前一个结构的地址存放到previous指针中。在第一次迭代中,previous的指针是NULL,所以什么也不做。
在完成了所有的输入语句,到循环的最后,有下面两行语句:
current->next = NULL; /* In case it's the last…*/
previous = current; /* Save address of last horse */
在current指向的结构中,next指针设定成NULL,表示这是最后一个结构,没有下一个结构了。如果有下一个结构,指针next会在下一次迭代时修改。指针previous设定成current,然后进入下一次迭代,此时current指向的结构就是previous指向的结构了。
这个程序的优点是生成了horse结构链,在这个链中,每个结构的next成员都指向下一个结构。最后一个结构例外,因为再也没有下一个horse结构了,所以next指针包含NULL,这称为链表。
horse数据放在链表中后,就可以从第一个结构开始,通过指针成员next访问下一个结构。指针next是NULL时,就到达了链表的末尾。这就是为所有输入生成输出表的方式。
在需要处理数量未知的结构的应用程序中,链表非常有用。链表的主要优点是内存的使用和便于处理。存储和处理链表所占用的内存量最少。即使所使用的内存比较分散,也可以从一个结构进入下一个结构。因此,链表可以用于同时处理几个不同类型的对象,每个对象都可以用它自己的链表来处理,以优化内存的使用。但链表也有一个小缺点:数据处理的速度比较慢,尤其是要随机访问数据时,速度更慢。
输出过程说明了如何遍历链表,以访问它,语句如下:
current = first; /* Start at the beginning */
while (current != NULL) /* As long as we have a valid pointer */
{ /* Output the data*/
printf("\n\n%s is %d years old, %d hands high,",
current->name, current->age, current->height);
printf(" and has %s and %s as parents.", current->father,
current->mother);
previous = current; /* Save the pointer so we can free memory */
current = current->next; /* Get the pointer to the next */
free(previous); /* Free memory for the old one */
}
输出循环由current指针控制,它开始时设定成first。而first指针包含链表中第一个结构的地址。循环会遍历链表,显示每个结构的成员,之后把current赋予指向下一个结构的成员next。
结构显示过后就释放其内存。这是很重要的,一旦不再需要引用结构,就释放其内存。但是不能在输出当前结构的所有成员后,马上调用free()函数。必须先引用当前结构的next成员,得到下一个horse结构的指针。
在链表的最后一个结构中,next指针是NULL,因而结束循环。
11.2.4 双向链表
前一个例子创建的链表有一个缺点:只能往前走。其实,只需小小的修改,就可以得到双向链表(doubly linked list),可以双向遍历链表。方法是除了指向下一个结构的指针外,在每个结构中再添加一个指针,存储前一个结构的地址。
试试看:双向链表
修改程序11.4,改成双向链表:
/* Program 11.5 Daisy chaining the horses both ways */
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
int main(void)
{
struct horse /* Structure declaration */
{
int age;
int height;
char name[20];
char father[20];
char mother[20];
struct horse *next; /* Pointer to next structure */
struct horse *previous; /* Pointer to previous structure */
};
struct horse *first = NULL; /* Pointer to first horse */
struct horse *current = NULL; /* Pointer to current horse */
struct horse *last = NULL; /* Pointer to previous horse */
char test = '\0'; /* Test value for ending input */
for( ; ; )
{
printf("\nDo you want to enter details of a%s horse (Y or N)? ",
first == NULL?"nother " : "");
scanf(" %c", &test );
if(tolower(test) == 'n')
break;
/* Allocate memory for each new horse structure */
current = (struct horse*)malloc(sizeof(struct horse));
if( first == NULL )
{
first = current; /* Set pointer to first horse */
current->previous = NULL;
}
else
{
last->next = current; /* Set next address for previous horse */
current->previous = last; /* Previous address for current horse */
}
printf("\nEnter the name of the horse: ");
scanf("%s", current -> name ); /* Read the horse's name */
printf("\nHow old is %s? ", current -> name);
scanf("%d", ¤t -> age); /* Read the horse's age */
printf("\nHow high is %s ( in hands )? ", current -> name);
scanf("%d", ¤t -> height); /* Read the horse's height */
printf("\nWho is %s's father? ", current -> name);
scanf("%s", current -> father); /* Get the father's name */
printf("\nWho is %s's mother? ", current -> name);
scanf("%s", current -> mother); /* Get the mother's name */
current -> next = NULL; /* In case it's the last horse..*/
last = current; /* Save address of last horse */
}
/* Now tell them what we know. */
while(current != NULL) /* Output horse data in reverse order */
{
printf("\n\n%s is %d years old, %d hands high,",
current->name, current->age, current->height);
printf(" and has %s and %s as parents.", current->father,
current->mother);
last = current; /* Save pointer to enable memory to be freed */
current = current->previous; /* current points to previous in list */
free(last); /* Free memory for the horse we output */
}
return 0;
}
如果输入相同的数据,这个程序会产生和前一个例子相同的结果,只是显示的顺序相反。
代码的说明
开始的指针声明如下:
struct horse *first = NULL; /* Pointer to first horse */
struct horse *current = NULL; /* Pointer to current horse */
struct horse *last = NULL; /* Pointer to previous horse */
把在循环的上一个迭代中输入的horse结构指针名称改成last。这么做并不是必需的,但有助于避免和horse结构中的成员previous混淆。
horse结构的声明如下:
struct horse /* Structure declaration */
{
int age;
int height;
char name[20];
char father[20];
char mother[20];
struct horse *next; /* Pointer to next structure */
struct horse *previous; /* Pointer to previous structure */
};
现在horse结构有两个指针,一个是往前的指针称为next,另一个是往后的指针称为previous。这样就可以双向遍历链表,这也是在程序的最后可以反向输出数据的原因。
除了输出之外,程序的唯一变化是在输入循环的开头添加了使用结构成员指针previous的语句:
if( first == NULL )
{
first = current; /* Set pointer to first horse */
current->previous = NULL;
}
else
{
last->next = current; /* Set next address for previous horse */
current->previous = last; /* Previous address for current horse */
}
这里用if-else取代上一个例子中的两个if语句。唯一的区别是设定了结构成员previous的值。第一个结构的previous设定成NULL,其后的结构都把previous设定成last,last的值是在前一次迭代中存储的。
另一个改变是在输入循环的最后。
last = current; /* Save address of last horse */
添加这行语句,是为了把下一个结构的previous指针设定成相应的值,即变量last中存储的current结构。
输出过程基本上和前一个例子相同,只是从链表中的最后一个结构开始,遍历到第一个结构而已。
11.2.5 结构中的位字段
位字段(bit-fields)提供的机制允许定义变量来表示一个整数中的一个或多个位,这样,就不需要为每个位明确指定成员名称了。
注意:
位字段常用在必须节省内存的情况下。这种情况目前比较少见。与标准类型的变量相比,位字段会明显降低程序执行的速度。因此,必须在节省内存和程序执行速度之间作一个抉择。在大多数情况下,不需要使用位字段,使用它甚至是不理想的,但读者应了解它。
下面是一个声明位字段的例子:
struct
{
unsigned int flag1 : 1;
unsigned int flag2 : 1;
unsigned int flag3 : 2;
unsigned int flag4 : 3;
} indicators;
上述语句定义了indicators变量,它是匿名结构的一个实例,包含4个位字段,分别是flagl~flag4。它们全部存储在一个字符组(word)中,如图11-4所示。

图11-4 结构中的位字段
前两个位字段在定义中指定为1,表示它们是一个位,其值是0或1。第三个位字段flag3有两个位,其值是0~3。最后一个flag4有三个位,其值是0~7。引用这些位字段的方式和引用一般结构成员的方式相同。例如:
indicators.flag4 = 5;
indicators.flag3 = indicators.flag1 = 1;
几乎没什么机会用到这个功能,这里介绍它只是为了讨论完整性,如果哪天缺乏内存,就可以考虑使用它。





