前言
结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。与 Java 中的类类似,可以有成员,包括变量和函数(通过函数指针实现)。
一、结构体 struct
关键字 struct 能定义各种类型的变量集合,称为结构(structure),并把它们视为一个单元。以下是一个简单的结构声明例子:
struct Person {
int age;
int height;
} serah;
该例子声明了一个结构类型 Person,它是一个新的类型,类型名称通常称为结构标记符(structure tag)或标记符名称(tag name)。其内部的变量 age 和 height 称为成员(members)或字段。
在结构体中的成员可以使任何类型的变量,要注意的是初始化不能放在这里,因为现在是定义类型的成员,而不是在声明变量。结构类型是一种说明或一种蓝图,可以用于定义该类型的变量。
1.1、定义结构类型和结构变量
可以将结构的声明和结构变量的声明分开,如下所示:
struct Person {
int age;
int height;
char name[20];
char mate[20];
};
struct Person lilith = {17, 172, "Lilith", "Adam"};
定义存储结构的新变量时,需要 struct 关键字,但没有关键字,代码看起来更加简单、更容易理解。使用 typedef 定义,声明变量时就可以删除 struct 关键字。例如:
typedef struct Person Person;
这个语句把 Person 定义为 struct Person。如果把这个定义放在源文件开头,就可以定义 Person 类型的变量:
Person lilith = {17, 172, "Lilith", "Adam"};
不需要在使用关键字 struct,使代码更加简洁,看起来像是普通的类型。
1.2、访问结构成员
结构变量的名称不是一个指针,所以需要特殊的语法访问这些成员。成员的引用方式为:在结构体变量名称后面加“.”再加上成员变量名称。例如:
lilith.age = 17;
结构变量名称混合成员变量名称之间的句点称为“成员选择运算符”。在上一小结中结构变量的初始化需要以正确的顺序获得字段的初始值,这对于 Person struct 不成问题,但结构若有很多成员,就有问题了。
其实在初始化列表中可以指定成员名,如下:
Person lilith = {
.height = 172, .age = 17, .name = "Lilith",
.mate = "Adam"
};
1.3、未命名的结构(匿名结构)
用一条语句声明结构和该结构的实例时,可以省略标记符的名字,以之前的例子为例:
struct
{
int age;
int height;
char name[20];
char mate[20];
} lilith;
使用这种方式的最大缺点是不能在其他语句中定义这个结构的其他实例。这个结构类型的所有变量必须在一行语句中定义。
1.4、结构指针
要获取结构的地址,就需要使用结构的指针。由于需要的是结构的地址,因此就需要声明结构的指针。结构指针的声明方式和声明其他类型的指针变量相同,例如:
Person *adam = NULL;
以上语句声明了一个 Person 类型指针,它可以存储 Person 类型的结构地址。现在可以将 adam 设置为一个特定结构的地址值,使用的方法和其他类型的指针完全相同。
示例代码:
Person lilith = {17, 12, "Lilith","Adam"};
lilith.age = 18;
h1.height = 173;
Person *eve;
eve = &lilith;
printf("lilith: name is %s, age is %d, height is %d.\n", lilith.name, lilith.age, lilith.height);
printf("eve: name is %s, age is %d, height is %d.\n", eve->name, eve->age, eve->height);
-> 等价于 (*XXX),执行结果:
lilith: name is Lilith, age is 18, height is 173.
eve: name is Lilith, age is 18, height is 173.
1.5、为结构动态分配内存
要为结构动态分配内存,可以使用结构指针数组,其声名如下:
Person* angels[7];
以上代码声明了 7 个指向 Person 结构的指针数组。该语句只给指针分配了内存。还需要分配一些内存来存储每个结构的成员。
示例代码:
#include
#include
#include
typedef struct Person Person;
struct Person
{
int age;
int height;
char name[20];
char mate[20];
};
int main(void) {
Person* angels[7];
int pcount = 0;
char test = '\0';
for(pcount = 0; pcount < sizeof(pcount)/sizeof(Person*); ++pcount) {
printf("Do you want to enter details of a%s angel (Y or N)?", pcount?"nother" : "");
scanf(" %c", &test);
if(tolower(test) == 'n') break;
// 动态分配内存
angels[pcount] = (Person*)malloc(sizeof(Person));
printf("Enter the name of the angel:");
scanf("%s", angels[pcount]->name);
printf("How old of the %s? ", angel[pcount]->name);
scanf("%d", &angels[pcount]->age); // -> 优先级大于 &
printf("How height of the %s? ", angels[pcount]->name);
scanf("%d", &angels[pcount]->height);
}
printf("\n");
for(int i = 0; i < pcount; ++i) {
printf("%s is %d years old, %d hands high, ", angels[i]->name, angels[i]->age, angels[i]->height);
free(angels[i]); // 注意释放动态分配的内存
}
return 0;
}
该示例代码与之前的类似,但是其运行并不相同。一开始并没有为任何结构分配内存。仅仅是声明了 7 个 Person 类型的指针,还要井结构放在指针指向的地址中,以下语句会为每个结构体分配内存空间:
angels[pcount] = (Person*)malloc(sizeof(Person));
这个例子给 Person 类型使用 sizeof 运算符来提供变元。使用 sizeof 运算符可以计算出结构体所占的字节数,其结构不一定对应于结构中各个成员所占的字节总数和。除了 char 类型变量外, 2 字节变量的启始地址常常是 2 的整数被,四字节变量的启始地址常常是 4 的整数倍,以此类推。这称为边界调整(boundary alignment),它和 C 语言无关,而是硬件要求。以这种方式在内存中存储变量,可
以更快地在处理器和内存之间传递数据,但不同类型的成员变量之间会有未使用的字节。详情请参考《深入理解计算机操作系统第》三章和第九章相关内容。
注意:C 的 _Alignof 运算符可以用于确定变量的边界调整量。在变量的声明中使用 _Alignof(type),可以强制根据特定的类型进行边界调整。
二、结构成员
所有基本数据类型(含数组)都可以成为结构的成员。除此之外,还可以把一个结构作为为另一个结构的成员,不仅指针可以是结构的成员,结构指针也可以是结构的成员。而这也增加了潜在的危险。
2.1、将结构作为另一个结构的成员
看以下示例代码:
结构体:
typedef struct Date Date;
struct Date
{
int day;
int month;
int year;
};
struct Person
{
Date dob;
int height;
char name[20];
char mate[20];;
}
使用:
Person adam;
adam.height = 175;
adam.dob.day = 24;
adam.dob.month = 12;
adam.dob.year = 0;
Person eve;
eve.dob = adam.dob;
2.2、声明结构中的结构
可以在 Person 结构的定义中声明 Date 结构,如下:
struct Person
{
struct Date
{
int day;
int month;
int year;
} dob;
int height;
char name[20];
char mate[20];
};
这个声明将 Date 结构声明放在结构 Person 的定义中,因此不能在 Person 结构的外部声明 Date 变量。当然,每个 Person 类型的变量都包含 Date 类型的成员 dob。但是以下语句会导致编译错误:
struct Date date;
如果需要在 Person 结构的外部使用 Date,就必须将它定义在 Person 结构之外。
2.3、将结构指针用于结构成员
任何指针都可以是结构的成员,包含结构指针在内。结构指针可以指向相同类型的结构(例如链表)。示例代码:
struct Person
{
int age;
int height;
Person* next;
};
将以上代码中结构 Person 添加前向 Person 指针就可以构成双向链表,这样就可以进行双向遍历。
struct Person
{
int age;
int height;
char name[20];
Person* next;
Person* previous;
};
三、结构与函数
3.1、结构作为函数变元
将结构作为变元传给函数和传递一般变量并没用什么不同,声明结构:
struct Family
{
char name[20];
int age;
char father[20];
char monther[20];
};
定义函数变元类型为结构,代码如下:
bool siblings(Family member1, Family member2) {
if(strcmp(member1.monther, member2.monther) == 0) return true;
else return false;
}
3.2、结构指针作为函数变量
在调用函数时,传送给变元的是变元的副本。如果变元是一个非常大的结构,就需要消耗大量的时间和内存空间。在这种情况下可以使用结构指针作为变元。重写以上示例:
bool siblings(Family* member1, Family* member2) {
if(strcmp(member1->monther, member2->monther) == 0) return true;
else return false;
}
但是这又一个缺点,按值传递机制禁止在被调用的函数中意外地改变变元值。如果使用指针,就丧失了这个优点。如果不需要改变指针变元的值(只是访问使用),把指针传送函数还是可以获得某种程度的保护,此时应使用
const 修饰符。重写以上函数:
bool siblings(Family const *member1, Family const *member2) {
if(strcmp(member1->monther, member2->monther) == 0) return true;
else return false;
}
注意:
- Family const member1:指向 Family 结构类型的常量指针*;
- Family const *member1:指向常量结构的指针;
3.3、作为函数返回值的结构
函数返回结构和返回一般数值一样,可以从函数中返回一个结构,但是比较方便的做法是返回结构指针。当然,在返回结构指针时,结构应在堆上创建。下面通过实例讨论其细节。
示例代码:
#include
#include
#include
#include
typedef struct Family Family;
typedef struct Date Date;
Family *get_person(void);
void show_people(bool forwards, Family *pf, Family *pl);
void release_memory(Family *pf);
struct Date
{
int day;
int month;
int year;
};
struct Family
{
Date dob;
char name[20];
char father[20];
char monther[20];
Family* next;
Family* previous;
};
int main(void) {
Family* first = NULL;
Family* current = NULL;
Family* last = NULL;
char more = '\0';
while(true) {
printf("\nDo you want to enter details of a%s person (Y or N)?", first != NULL?"nother" : "");
scanf(" %c", &more);
if(tolower(more) == 'n') break;
current = get_person();
if(first == NULL) first = current;
else {
last->next = current;
current->previous = last;
}
last = current;
}
show_people(true, first, last);
release_memory(first);
first = last = NULL;
return 0;
}
Family *get_person(void) {
Family *temp = (Family*)malloc(sizeof(Family));
printf("Enter the name of the person:");
scanf("%s", temp->name);
printf("Enter %s's date of birth (day month year); ", temp->name);
scanf("%d %d %d", &temp->dob.day, &temp->dob.month, &temp->dob.year);
printf("\nWho is %s's father? ", temp->name);
scanf("%s", temp->father);
printf("\nWho is %s's monther? ", temp->name);
scanf("%s", temp->monther);
temp->next = temp->previous = NULL;
return temp;
}
void show_people(bool forwards, Family *pf, Family *pl) {
printf("\n");
for(Family *p = forwards ? pf : pl; p != NULL; p = forwards ? p->next : p->previous) {
printf("%s was born %d%d%d and has %s and %s as parents.\n",
p->name, p->dob.day, p->dob.month, p->dob.year, p->father, p->monther);
}
}
void release_memory(Family *pfirst) {
Family *pcurrent = pfirst;
Family *temp = NULL;
while(pcurrent) {
temp = pcurrent;
pcurrent = pcurrent->next;
free(temp);
}
}
重点看 get_person() 函数,它首先是执行动态分配堆内存:
Family *temp = (Family*)malloc(sizeof(Family));
temp 是 “Family 类型结构的指针”,并且是本地对象,只存在于函数体内。以上语句给 Family 类型分配内存空间,并将返回地址保存在了指针变量 temp 中。temp 是本地变量,在 get_person() 函数结束时,temp 就不存在了,但是 malloc() 函数分配的内存是永久的,可以在程序的某个地方释放该内存,或在退出程序时释放它。
get_person() 的最后一条语句为:
return temp;
这行语句返回结构指针的副本。
参考
Beginning C
深入理解计算机操作系统