前面学习了如何声明和定义变量,使之包含各种类型的数据,如整数、浮点数和字符等。学习了如何创建这些类型的数组及指针数组,这些指针指向包含可用数据类型的内存位置。这些很有用,但是许多应用程序还需要一些更灵活的功能。
例如,要编写一个处理马匹数据的程序,就需要每匹马的名字、出生日期、颜色、高度和它的父母等。在这些数据中,一些项是字符串,一些项是数值。因此,必须为每一种数据类型建立数组并存储它们。但这是有限制的,例如不能方便地引用Dobbin的生日或Trigger的身高。必须通过一个通用索引将数据项关联起来,使数组同步。C语言在这方面提供了相当好的方法,这也是本章将要讨论的主题。
本章的主要内容:
● 什么是数据结构
● 如何声明并定义数据结构
● 如何使用结构和结构指针
● 如何将指针作为结构的成员
● 如何在变量间共享内存
● 如何定义自己的数据类型
● 如何编写程序,根据数据生成条形图
11.1 数据结构:使用struct
关键字struct能定义各种类型的变量集合,称为结构(structure),并把它们视为一个单元。下面是一个简单的结构声明例子:
struct horse
{
int age;
int height;
} Silver;
这个例子声明了一个结构horse。horse不是一个变量名,而是一个新的类型,这个类型名称通常称为结构标记符(structure tag)或标记符名称(tag name)。结构标记符的命名方式和我们熟悉的变量名相同。
注意:
结构标记符可以和变量使用相同的名称,但最好不要这么做,因为这会使代码难以理解。
结构内的变量名称age和height称为结构成员(structure members)。在这个例子中,它们都是int类型。结构成员出现在结构标记符名称horse后的大括号内。
在这个结构例子中,结构的一个实例Silver是在定义结构时声明的。它是一个horse类型的变量,只要使用变量名称Silver,它都包含两个结构成员:age和height。
下面是horse结构类型的稍微复杂的声明:
struct horse
{
int age;
int height;
char name[20];
char father[20];
char mother[20];
} Dobbin = {
24, 17, "Dobbin", "Trigger", "Flossie"
};
结构内的成员可以是任何类型的变量,包含数组在内。在horse结构类型的这个版本中,有5个成员:整数成员age和height,数组成员name、father和mother。每个成员的声明方式和一般变量的声明方式相同,都是先声明类型,然后是名称,最后用分号结束。注意,初始化值不能放在这里,因为现在是定义horse类型的成员,而不是在声明变量。结构类型是一种说明或一种蓝图,可以用于定义该类型的变量—— 就这个例子而言,类型是horse。
在horse结构定义的闭括号后定义了一个实例变量Dobbin。给Dobbin赋予初始值的方式和数组类似,所以在定义horse类型的实例时,可以指定初始值。
在Dobbin变量的声明中,最后一对大括号内的值按顺序赋予成员变量age(24)、height(17)、name("Dobbin")、father("Trigger")和mother("Flossie")。该语句用分号结束。变量Dobbin现在引用了结构内所有的成员。结构Dobbin占用的内存如图11-1所示(假定int类型的变量占4个字节)。通常可以使用sizeof运算符计算出结构占用的内存量。

图11-1 Dobbin占用的内存
11.1.1 定义结构类型和结构变量
可以将结构的声明和结构变量的声明分开。取代前面例子的语句如下:
struct horse
{
int age;
int height;
char name[20];
char father[20];
char mother[20];
};
struct horse Dobbin = {
24, l7, "Dobbin", "Trigger", "Flossie"
};
现在有两个分开的语句。第一个定义结构标记符horse,第二个声明该类型的变量Dobbin。结构定义和结构变量声明语句都用分号结束。在Dobbin结构成员的初始值中,Dobbin的父亲是Trigger,母亲是Flossie。
也可以给前面两个例子添加第三条语句,定义另一个horse类型的变量:
struct horse Trigger = {
30, 15, "Trigger", "Smith", "Wesson"
};
现在有一个变量Trigger,它包含Dobbin父亲的数据,显然,Trigger的父母是Smith和Wesson。
当然,也可以在一行语句中声明多个结构变量。声明的方式和声明C语言标准类型的多个变量一样。例如:
struct horse Piebald, Bandy;
这行语句声明了两个horse类型的变量。比起标准类型的声明,这个声明只增加了关键字struct。为了使这行语句简单,没有初始化变量,不过一般应初始化变量。
11.1.2 访问结构成员
现在知道如何定义结构及声明结构变量了,还必须引用结构的成员。结构变量的名称不是一个指针,所以需要特殊的语法访问这些成员。
要引用结构成员,应在结构变量名称的后面加上一个句点,再加上成员变量名称。例如,发现Dobbin隐瞒了它的年龄,事实上它比初始化的值年轻,就可以将值修正如下:
Dobbin.age = 12;
结构变量名称和成员名称间的句点是一个运算符,称为成员选择运算符。这行语句将Dobbin结构的age成员设定成12。结构成员和相同类型的变量完全一样,可以给它们设定值,也可以在表达式中像使用一般变量一样使用它们。
试试看:使用结构
尝试将前面所学的horse结构用于一个简单的例子:
/* Program 11.1 Exercising the horse */
#include <stdio.h>
int main(void)
{
/* Structure declaration */
struct horse
{
int age;
int height;
char name[20];
char father[20];
char mother[20];
};
struct horse My_first_horse; /* Structure variable declaration */
/* Initialize the structure variable from input data */
printf("Enter the name of the horse: " );
scanf("%s", My_first_horse.name ); /* Read the horse's name */
printf("How old is %s? ", My_first_horse.name );
scanf("%d", &My_first_horse.age ); /* Read the horse's age */
printf("How high is %s ( in hands )? ", My_first_horse.name );
scanf("%d", &My_first_horse.height ); /* Read the horse's height */
printf("Who is %s's father? ", My_first_horse.name );
scanf("%s", My_first_horse.father ); /* Get the father's name */
printf("Who is %s's mother? ", My_first_horse.name );
scanf("%s", My_first_horse.mother ); /* Get the mother's name */
/* Now tell them what we know */
printf("\n%s is %d years old, %d hands high,",
My_first_horse.name, My_first_horse.age, My_first_horse.height);
printf(" and has %s and %s as parents.\n", My_first_horse.father,
My_first_horse.mother );
return 0;
}
根据输入的数据,得到的输出如下:
Enter the name of the horse: Neddy
How old is Neddy? 12
How high is Neddy ( in hands )? 14
Who is Neddy's father? Bertie
Who is Neddy's mother? Nellie
Neddy is 12 years old, 14 hands high, and has Bertie and Nellie as parents.
代码的说明
引用结构成员的方式使这个例子非常容易理解。用下面的语句定义horse结构:
struct horse
{
int age;
int height;
char name[20];
char father[20];
char mother[20];
};
这个结构有两个整数成员age和height,以及三个字符数组成员name、father和mother。在闭括号的后面仅是一个分号,还没有声明horse类型的变量。在定义完horse结构后,具有如下语句:
struct horse My_first_horse; /* Structure variable declaration */
这行语句声明My_first_horse是一个horse类型的变量,没有指定初值。
然后,使用下面的语句为My_first_horse结构的成员name读入数据:
scanf("%s", My_first_horse.name ); /* Read the horse's name */
这里不需要使用寻址运算符(&),因为结构的成员name是一个数组,所以将数组第一个元素的地址隐式传送给函数scanf()。要引用结构成员,应使用结构名称My_first_horse,后跟一个句点和成员的名称name。访问结构成员时,除了表示方法不同外,其他的和一般变量完全相同。
接下来给horse的成员age读入数值:
scanf("%d", &My_first_horse.age ); /* Read the horse's age */
由于这个成员是int类型的变量,所以必须使用&运算符传递这个结构成员的地址。
注意:
对struct对象的成员使用寻址运算符时,要将&放在成员的引用之前,而不是放在成员名称之前。
后面的语句使用相同的方式为结构的其他成员读入数据,并对每个输入显示提示。输入完成后,就使用下面的语句将读入的数值输出到一行上。
printf("\n%s is %d years old, %d hands high,",
My_first_horse.name, My_first_horse.age, My_first_horse.height);
printf(" and has %s and %s as parents.\n", My_first_horse.father,
My_first_horse.mother );
引用结构成员的名字很长,使语句看起来很复杂,其实它是相当简单的。程序使用变量成员的名字作为函数的第一个参数,这是以前介绍的格式控制字符串的标准形式。
11.1.3 未命名的结构
不—定要给结构指定标记符名字。用一条语句声明结构和该结构的实例时,可以省略标记符名字。在上一个例子中,声明了horse类型和该类型的实例My_first_horse,也可以改为:
struct
{ /* Structure declaration and... */
int age;
int height;
char name[20];
char father[20];
char mother[20];
} My_first_horse; /* ...structure variable declaration combined */
使用这种方法的最大缺点是不能在其他语句中定义这个结构的其他实例。这个结构类型的所有变量必须在一行语句中定义。
11.1.4 结构数组
保存马匹数据的基本方法就是这样,但在处理50或100匹马如此大量的数据时会比较麻烦,此时需要一个更可靠的方法去处理大量的马匹数据。使用变量也会遇到这个问题。此时解决方法是使用数组,这里也可以声明一个horse数组。
试试看:使用结构数组
扩展前一个例子,以处理几匹马的数据:
/* Program 11.2 Exercising the horses */
#include <stdio.h>
#include <ctype.h>
int main(void)
{
struct horse /* Structure declaration */
{
int age;
int height;
char name[20];
char father[20];
char mother[20];
};
struct horse My_horses[50]; /* Structure array declaration */
int hcount = 0; /* Count of the number of horses */
char test = '\0'; /* Test value for ending */
for(hcount = 0; hcount<50 ; hcount++ )
{
printf("\nDo you want to enter details of a%s horse (Y or N)? ",
hcount?"nother " : "" );
scanf(" %c", &test );
if(tolower(test) == 'n')
break;
printf("\nEnter the name of the horse: " );
scanf("%s", My_horses[hcount].name ); /* Read the horse's name */
printf("\nHow old is %s? ", My_horses[hcount].name );
scanf("%d", &My_horses[hcount].age ); /* Read the horse's age */
printf("\nHow high is %s ( in hands )? ", My_horses[hcount].name );
/* Read the horse's height*/
scanf("%d", &My_horses[hcount].height );
printf("\nWho is %s's father? ", My_horses[hcount].name );
/* Get the father's name */
scanf("%s", My_horses[hcount].father );
printf("\nWho is %s's mother? ", My_horses[hcount].name );
/* Get the mother's name */
scanf("%s", My_horses[hcount].mother );
}
/* Now tell them what we know. */
for(int i = 0 ; i<hcount ; i++ )
{
printf("\n\n%s is %d years old, %d hands high,",
My_horses[i].name, My_horses[i].age, My_horses[i].height);
printf(" and has %s and %s as parents.", My_horses[i].father,
My_horses[i].mother );
}
return 0;
}
这个程序的输出和前一个只处理一匹马的例子有点不同。输入每匹马的数据时,都会显示提示。50匹马的数据输入完后,程序就输出所有数据的小结。整个机制是稳定的,运行良好(几乎总是成功)。
代码的说明
在这个马匹数据处理版本中,首先声明horse结构,如下:
struct horse /* Structure declaration */
这条语句声明变量My_horses是一个有50个horse结构的数组。除了关键字struct外,这个数组声明与其他的数组声明都相同。
然后是一个用变量hcount控制的for循环:
for(hcount = 0; hcount < 50 ; hcount++ )
{
…
}
这个循环让程序读入50匹马的数据。循环控制变量hcount用来累加horse结构的总数。循环内的第一个动作是:
printf("\nDo you want to enter details of a%s horse (Y or N)? ",
hcount?"nother " : "" );
scanf(" %c", &test );
if(tolower(test) == 'n')
break;
每次迭代都要求用户输入Y或N,指定是否输入另一匹马的数据。在第一次之后的每次迭代中,printf()语句都使用条件运算符在输出中插入"nother"。在使用scanf()读完用户输入的字符后,如果用户的响应是否定的,if语句就会执行break语句,跳出循环。
接下来的一串printf()同scanf()同以前一样,但有两点需要注意,如下面的语句:
scanf("%s", My_horses[hcount].name ); /* Read the horse's name */
从上述语句可以看出,引用结构数组的一个元素成员的方法非常简单。这个结构数组名称将索引放在方括号内,后跟句点和成员名。如果想引用这个结构第4个元素的name数组的第3个元素,可以使用:
My_horses[3].name[2]
注意:
结构数组的索引与其他类型的数组一样,也是从0开始,所以结构数组的第4个元素的索引值是3,而其成员数组的第3个元素的索引值是2。
现在看看下面的语句:
scanf("%d", My_horses[hcount].age); /* Read the horse's age */
注意,如果传给scanf()的变元是字符串数组变量,就不需要寻址运算符,如My_horses [hcount].name。但如果变元是整数,如My_horses[hcount].age和My_horses[hcount].height,就必须使用寻址运算符。在读入变量值时很容易忘掉&,所以要特别注意。
前面介绍的结构不仅适用于马匹数据的应用程序,也适用于猪或驴等的数据处理。
11.1.5 表达式中的结构
结构中的成员可以像一般变量那样用于表达式。以程序11.2中的结构为例,可以将它们用在下面的表达式中:
My_horses[1].height = (My_horses[2].height + My_horses[3].height]/2;
—匹马的高度是另两匹马的平均高度是没什么道理的,但这是一个合法的语句。也可以在赋值语句中使用整个结构元素。
My_horses[1] = My horses[2];
这行语句会将结构My_horses[2]的所有成员复制到结构My_horses[1]中,使这两个结构完全相同。使用整个结构的另一个操作是使用&运算符提取地址。但是不能对整个结构执行加、比较或其他操作。为此,必须编写定制的函数。
11.1.6 结构指针
要获得结构的地址,就需要使用结构的指针。由于需要的是结构的地址,因此需要声明结构的指针。结构指针的声明方式和声明其他类型的指针变量相同,例如:
struct horse *phorse;
这条语句声明了一个phorse指针,它可以存储horse类型的结构地址。现在可以将phorse设置为一个特定结构的地址值,使用的方法和其他类型的指针完全相同,例如:
phorse = &My_horses[1];
现在phorse指向结构My_horses[1])。可以通过phorse指针引用这个结构的元素。因此,如果要显示这个结构成员的名字,可以编写如下语句:
printf("\nThe name is %s.", (*phorse).name);
取消引用指针的括号是非常重要的,因为成员选择运算符(句点)的优先级高于取消引用指针运算符*。这个操作还有另一种方法,且更容易理解。将上面的语句改写成:
printf("\nThe name is %s.", phorse->name);
这就不需要括号或星号了。–>运算符是一个负号后跟一个大于号。这个运算符有时也称为成员指针运算符。这个表示法几乎可用于取代通常的取消引用指针表示法。因为这个运算符使程序更容易理解。
11.1.7 为结构动态分配内存
可以利用前面掌握的各种工具重写程序11.2,以更经济的方式使用内存。程序11.2的最初版本为包含50个horse结构的数组分配了内存,而实际上并不需要这么多内存。
要为结构动态分配内存,可以使用结构指针数组,其声明非常简单,如下所示:
struct horse *phorse[50];
这行语句声明了50个指向horse结构的指针数组。该语句只给指针分配了内存。还需要分配一些内存来存储每个结构的成员。
试试看:使用结构指针
下面的例子演示了如何为结构动态分配内存:
/* Program 11.3 Pointing out the horses */
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h> /* For malloc() */
int main(void)
{
struct horse /* Structure declaration */
{
int age;
int height;
char name[20];
char father[20];
char mother[20];
};
struct horse *phorse[50]; /* pointer to structure array declaration */
int hcount = 0; /* Count of the number of horses */
char test = '\0'; /* Test value for ending input */
for(hcount = 0; hcount < 50 ; hcount++ )
{
printf("\nDo you want to enter details of a%s horse (Y or N)? ",
hcount?"nother " : "" );
scanf(" %c", &test );
if(tolower(test) == 'n')
break;
/* allocate memory to hold a structure */
phorse[hcount] = (struct horse*) malloc(sizeof(struct horse));
printf("\nEnter the name of the horse: " );
scanf("%s", phorse[hcount]->name ); /* Read the horse's name */
printf("\nHow old is %s? ", phorse[hcount]->name );
scanf("%d", &phorse[hcount]->age ); /* Read the horse's age */
printf("\nHow high is %s ( in hands )? ", phorse[hcount]->name );
scanf("%d", &phorse[hcount]->height ); /* Read the horse's height */
printf("\nWho is %s's father? ", phorse[hcount]->name );
scanf("%s", phorse[hcount]->father ); /* Get the father's name */
printf("\nWho is %s's mother? ", phorse[hcount]->name );
scanf("%s", phorse[hcount]->mother ); /* Get the mother's name */
}
/* Now tell them what we know. */
for(int i = 0 ; i < hcount ; i++ )
{
printf("\n\n%s is %d years old, %d hands high,",
phorse[i]->name, phorse[i]->age, phorse[i]->height);
printf(" and has %s and %s as parents.",
phorse[i]->father, phorse[i]->mother);
free(phorse[i]);
}
return 0;
}
输入和程序11.2相同的数据,则输出也相同。
代码的说明
这和前一个版本非常类似,但是其运行并不相同。一开始没有为任何结构分配内存。下面的声明:
struct horse *phorse[50]; /* pointer to structure array declaration */
仅定义了50个horse类型的结构指针,还要将结构放在指针指向的地址中,如下:
phorse[hcount] = (struct horse*) malloc(sizeof(struct horse));
这行语句会给每个结构分配内存空间。malloc()函数会分配变元指定的字节数,并将所分配内存块的地址返回为void类型的指针。这个例子使用sizeof运算符计算需要的字节数。
使用sizeof运算符可以计算出结构所占的字节数,其结果不一定对应于结构中各个成员所占的字节数总和,如果自己计算,就很容易出错。
除了char类型的变量之外,2字节变量的起始地址常常是2的倍数,4字节变量的起始地址常常是4的倍数,依此类推。这称为边界调整(boundary alignment),它和C语言无关,而是硬件的要求。以这种方式在内存中存储变量,可以更快地在处理器和内存之间传递数据,但不同类型的成员变量之间会有未使用的字节。这些未使用的字节也必须算在结构的字节数中,如图11-2所示。

图11-2 边界调整在内存分配上的影响
malloc()函数返回的值是一个void指针,因此必须用表达式(struct horse*)将它转换成所需要的类型。这样,这个指针在必要时就可以正确地递增或递减了。
scanf("%s", phorse[hcount]->name ); /* Read the horse's name */
这行语句使用新的表示法,通过指针选择结构的成员。它比 (*phorse[hcount]).name清楚得多。以后引用horse结构的成员都使用这种新的表示法。
最后,程序给每匹马的输入数据显示一个总结,然后释放内存。





