struct book {
char title[MAXTITL];
char author[MAXAUTL];
float value;
};
该声明并未创建实际的数据对象,只描述了该对象由什么组成。可以把这个声明放在所有函数的外部,那么该声明之后的所有函数都能使用;也可以放在一个函数定义的内部,这样只限于该函数内部使用。
结构布局告诉编译器如何表示数据,但是它并未让编译器为数据分配空间。
struct book library;
编译器使用该结构模板为该变量分配空间。
struct book library = {
"The Pious Pirate and the Devious Damsel",
"Renee Vivotte",
1.95
};
如果初始化静态存储期的变量(如,静态外部链接、静态内部链接或静态无链接),必须使用常量值。这同样适用于结构。如果初始化一个静态存储期的结构,初始化列表中的值必须是常量表达式。如果是自动存储期,初始化列表中的值可以不是常量。
// 只初始化value成员
struct book surprise = { .value = 10.99 };
// 按照任意顺序使用指定初始化器
struct book gift = {
.value = 25.99,
.author = "James Broadfool",
.title = "Rue for the Toad"
};
与数组类似,在指定初始化器后面的普通初始化器,为指定成员后面的成员提供初始值。另外,对特定成员的最后一次赋值才是它实际获得的值。
// value的实际值是0.25,因为它在结构声明中紧跟在author成员之后。
struct book gift = {
.value = 18.90,
.author = "Philionna Pestle",
0.25
};
如果程序创建了100个大小的自动存储类别的结构数组,其中的信息被储存在栈中,如此大的数组需要很大一块内存,此时可能会导致一些问题,需要使用编译器选项设置栈的大小,或者创建静态或外部数组以及减小数组的大小。
有时,在一个结构中包含另一个结构(即嵌套结构)很方便。
struct names {
char first[LEN];
char last[LEN];
};
struct guy {
// 嵌套结构
struct names handle;
char favfood[LEN];
char job[LEN];
float income;
};
int main(void) {
struct guy fellow = {
{ "Ewen", "Villard" },
"grilled salmon",
"personality coach",
68112.00
};
// ...
return 0;
}
// 可以指向任意现有的guy类型的结构
struct guy *him;
him = &barney;
当前的实现允许把结构作为参数使用,所以可以选择是传递结构本身,还是传递指向结构的指针。
现在的c允许把一个结构赋值给另一个结构,但是数组不能这样做:
// 如果n_data和o_data都是相同类型的结构
o_data = n_data;
即使成员是数组,也能完成赋值。另外,还可以把一个结构初始化为相同类型的另一个结构:
struct names right_field = { "Ruthie", "George" };
struct names captain = right_field;
还能把结构作为返回值返回。
把指针作为参数有两个优点:无论是以前还是现在的c实现都能使用这种方法,而且执行起来很快,只需要传递一个地址。缺点是无法保护数据。被调函数中的某些操作可能会意外影响原来结构中的数据。不过,新增的
const
限定符解决了这个问题。
把结构作为参数传递的优点是,函数处理的是原始数据的副本,这保护了原始数据。另外,代码风格也更清楚。
struct vector { double x; double y; };
// 如果用vector类型的结构ans储存相同类型结构a和b的和,就要把结构作为参数和返回值:
struct vector ans, a, b;
struct vector sum_vect(struct vector, struct vector);
// ...
ans = sum_vect(a, b);
// 相对于指针版本,上面的会更自然一些。并且,使用指针版本时,
// 程序员必须记住总和的地址应该是第1个参数还是第2个参数的地址。
void sum_vect(const struct vector *, const struct vector *, struct vector *);
sum_vect(&a, &b, &ans);
传递结构的两个缺点是:较老版本的实现可能无法处理这样的代码,而且传递结构浪费时间和存储空间。尤其是把大型结构传递给函数,而它只使用结构中的一两个成员时特别浪费。这种情况下传递指针或只传递所需的成员更合理。
#define LEN 20
struct names {
char first[LEN];
char last[LEN];
};
struct pnames {
char *first;
char *last;
};
struct names veep = { "Talia", "Summers" };
struct pnames treas = { "Brad", "Fallingjaw" };
printf("%s and %s\n", veep.first, treas.first);
对于
struct names
类型的结构变量veep
,以上字符串都被储存在结构内部,结构总共要分配40字节储存姓名。然而,对于struct pnames
类型的结构变量treas
,以上字符串储存在编译器储存常量的地方。结构本身只储存了两个地址,即16个字节。
考虑下面的代码:
struct names accountant;
struct pnames attorney;
puts("Enter the last name of your accountant:");
scanf("%s", accountant.last);
puts("Enter the last name of your attorney:");
scanf("%s", attorney.last); /* 这里有一个潜在的危险 */
对于
attorney
变量来说,scanf()
把字符串放到attorney.last
表示的地址上,由于这是未初始化的变量,地址可以是任何值,因此程序可以把值放在任何地方。如果走运的话,程序不会出问题,至少暂时不会,否则这一操作会导致程序崩溃。
因此,如果要用结构储存字符串,用字符串数组作为成员比较简单。用指向char
的指针也行,但是误用会导致严重的问题。
malloc()
如果使用
malloc()
分配内存并使用指针储存该地址,那么在结构中使用指针处理字符串就比较合理。这种方法的优点是,可以使用malloc()
为字符串分配合适的存储空间。
struct namect {
char *fname; // 用指针代替数组
char *lname;
int letters;
};
struct namect *pst;
char temp[SLEN];
printf("Please enter your first name.\n");
s_gets(temp, SLEN);
// 分配内存储存名
pst->fname = (char *) malloc(strlen(temp) + 1);
// 把名拷贝到已分配的内存
strcpy(pst->fname, temp);
printf("Please enter your last name.\n");
s_gets(temp, SLEN);
pst->lname = (char *) malloc(strlen(temp) + 1);
strcpy(pst->lname, temp);
需要注意的是,记得释放程序动态分配的内存。
C99的复合字面量特性可用于结构和数组。如果只需要一个临时结构值,复合字面量很好用。例如,可以使用复合字面量创建一个数组作为函数的参数或赋给另一个结构。
struct book {
char title[MAXTITL];
char author[MAXAUTL];
float value;
};
struct book readfirst;
readfirst = (struct book) { "The Idiot", "Fyodor Dostoyevsky", 6.99 }
struct rect {
double x;
double y;
};
double rect_area(struct rect r) { return r.x * r.y; }
double area = rect_area((struct rect) { 10.5, 20.0 });
double rect_areap(struct rect *rp) { return rp->x * rp->y; };
area = rect_areap(&(struct rect) { 10.5, 20.0 });
复合字面量在所有函数的外部,则具有静态存储期;如果在块中,则具有自动存储期。复合字面量中也可以使用指定初始化器。
C99新增了一个特性:伸缩型数组成员,利用这项特性声明的结构,其最后一个数组成员具有一些特性。
- 该数组不会立即存在。
- 使用这个伸缩型数组成员可以编写合适的代码,就好像它确实存在并具有所需数目的元素一样。
首先,声明一个伸缩型数组成员有如下规则:
- 伸缩型数组成员必须是结构的最后一个成员。
- 结构中必须至少有一个成员。
- 伸缩型数组的声明类似于普通数组,只是它的方括号中是空的。
struct flex {
int count;
double average;
// 伸缩型数组成员,不能用它做任何事,因为没有给这个数组预留存储空间。
double scores[];
};
实际上,C99的意图并不是让你声明
struct flex
类型的变量,而是希望声明一个指向该类型的指针,然后用malloc()
来分配足够的空间,以储存该类型结构的常规内容和伸缩型数组成员所需的额外空间:
struct flex *pf; // 声明一个指针
// 请求为一个结构和一个数组分配存储空间
pf = malloc(sizeof(struct flex) + 5 * sizeof(double));
pf->count = 5;
pf->scores[2] = 18.5;
带伸缩型数组成员的结构确实有一些特殊的处理要求。
- 不能用结构进行赋值或拷贝,这样做只能拷贝除伸缩型数组成员以外的其他成员。确实要进行拷贝,应使用
memcpy()
函数。
struct flex *pf1, *pf2; // *pf1和*pf2都是结构
// ...
*pf2 = *pf1; // 不要这样做
- 不要以按值方式把这种结构传递给结构。原因相同,按值传递一个参数与赋值类似。要把结构地址传递给函数。
- 不要使用带伸缩型数组成员的结构作为数组成员或另一个结构的成员。
匿名结构是一个没有名称的结构成员。
struct person {
int id;
struct { char first[20]; char last[20]; }; // 匿名结构
};
struct person ted = {8483, { "Ted", "Grass" }};
// 访问时简化了步骤
puts(ted.first);
联合是一种数据类型,它能在同一个内存空间中储存不同的数据类型(不是同时储存)。其典型的用法是,设计一种表以储存既无规律、事先也不知道顺序的混合类型。使用联合类型的数组,其中的联合都大小相等,每个联合可以储存各种数据类型。
// 只能储存一个int或一个double或char类型的值,编译器分配足够的空间
// 以便它能储存联合声明中占用最大字节的类型。
union hold {
int digit;
double bigfl;
char letter;
};
union hold valA;
valA.digit = 23;
valA.letter = 'R'; // 清除23,储存'R',占1字节。
union hold valB = valA; // 用另一个联合来初始化
union hold valC = { 88 }; // 初始化联合的digit成员
union hold valD = { .bigfl = 118.2 }; // 指定初始化器
联合的另一种用法是,在结构中储存与其成员有从属关系的信息。
struct owner {
char socsecurity[12];
// ...
};
struct leasecompany {
char name[40];
char headquaters[40];
// ...
};
union data {
struct owner owncar;
struct leasecompany leasecar;
};
struct car_data {
char make[15];
int status; /* 私有为0,租赁为1 */
union data ownerinfo;
// ...
};
struct car_data {
char make[15];
int status;
union {
struct owner owncar;
struct leasecompany leasecar;
};
// ...
};
// 如果flits是car_data类型的结构变量,可以用flits.owncar.socsecurity
// 代替flits.ownerinfo.owncar.socsecurity。
可以用枚举类型声明符号名称来表示整型常量。实际上,
enum
常量是int
类型,因此,只要能使用int
类型的地方就可以使用枚举类型。枚举类型的目的是提高程序的可读性:
enum spectrum { red, orange, yellow, green, blue, violet };
// 枚举类型的变量
enum spectrum color;
int c;
color = blue;
if (color == yellow) {
// ...
}
for (color = red; color <= violet; color++) {
// ...
}
需要注意的是,c枚举的一些特性并不适用于c++。例如,c允许枚举变量使用
++
运算符,但是c++标准不允许。所以,如果编写的代码将来会并入c++程序,那么必须将color
声明为int
类型,才能c和c++都兼容。
printf("red = %d, orange = %d\n", red, orange);
// 输出:red = 0, orange = 1
在枚举声明中,可以为枚举常量指定整数值:
enum levels { low = 100, medium = 500, high = 2000 };
如果只给一个枚举常量赋值,没有对后面的枚举常量赋值,那么后面的常量会被赋予后续的值:
// cat = 0, puma = 11, tiger = 12
enum feline { cat, lynx = 10, puma, tiger };
C语言使用名称空间标识程序中的各部分,即通过名词来识别。作用域是名称空间概念的一部分:两个不同作用域的同名变量不冲突;两个相同作用域的同名变量冲突。名称空间是分类别的。在特定作用域中的结构标记、联合标记和枚举标记都共享相同的名称空间,该名称空间与普通变量使用的空间不同。这意味着在相同作用域中变量和标记的名称可以相同,不会引起冲突,但是不能在相同作用域中声明两个同名标签或同名变量:
struct rect { double x; double y; };
int rect; // 在c中不会产生冲突
另外,c++不允许这样做,因为它把标记名和变量名放在相同的名称空间中。
typedef
由编译器解释,不是预处理器。
// 编译器把STRING解释成一个类型的标识符,该类型是指向char的指针。
typedef char *STRING;
通常,函数指针常用作另一个函数的参数,告诉该函数要使用哪一个函数。函数也有地址,因为函数的机器语言实现由载入内存的代码组成。指向函数的指针中储存着函数代码的起始处的地址。
为了指明函数类型,要指明函数签名,即函数的返回类型和形参类型:
void (*pf)(char *);
要声明一个指向特定类型函数的指针,可以先声明一个该类型的函数,然后把函数名替换成
(*pf)
形式的表达式。然后,pf
就成为指向该类型函数的指针。