第六章 结构体
结构体是一个或多个(可能是不同类型)的变量的集合,这些变量组合在单个名字下面,以便于处理。(结构体在其他语言中被称为"记录",典型的有Pascal。)结构体有助于组织数据,特别是在大型的程序中,因为它们允许把一组相关的变量当作一个单元,而不是当作各自独立的几个实体来分别处理。
结构体的一个传统例子是工资记录:雇员通过一组属性,如姓名、地址、社会安全号码和工资等来描述。其中的某些属性可能也是结构体:名字有多个组成部分,地址也是,甚至连工资也是。另一个例子来自图形,对 C 来说更为典型:一个点是一对坐标,矩形是一对点,等等。
ANSI 标准做的主要改变是定义了结构体的分配------结构体可以被拷贝、被赋值、被传递给函数,以及被函数返回。这些已经被大部分的编译器支持了很多年,但特性现在才被精确定义。自动结构体和数组现在也可以被初始化了。
6.1 结构体基础
我们首先来创建一些适用于图形的结构体。图形的基本对象是点,我们假设点有一个 x 坐标和一个 y 坐标,都是整数。
点的两个组成部分可以放在一个结构体里面,像下面这样声明:
struct point {
int x;
int y;
};
关键字 struct 引入了一个结构体声明,这是包在大括号中的一个声明列表。在 struct 后面可以跟一个可选的名字(这里是 point ),称为结构体标签(structure tag)。标签对这个结构体命名,这个标签后续可以作为声明中大括号部分的简写来使用。
结构体中的变量名称为成员(member)。结构体成员或标签,与通常的(即非成员的)变量可以有相同的名字而不会冲突,因为它们总是能通过上下文来区分。此外,同样的成员名称可以在不同的结构体中出现,不过在代码风格上,我们通常只会对密切相关的对象使用同一个名字。
struct 声明定义了一种类型。结构体成员列表之后的右大括号后面可以跟着变量列表,正和其他任意基本类型一样。即可以说
struct { ... } x, y, z;
在语法上类似于
int x, y, z;
因为这两个语句都声明了 x,y,z 是所指定类型的对象,而且为它们分配了内存空间。
后面不带变量列表的结构声明不会分配空间;它仅仅是描述了结构体的模板或者说形态。然而,如果声明带了标签,则标签后续可以用来定义这种结构体的实例。例如,有了上面的 point 声明后,
struct point pt;
定义了一个变量 pt ,其类型为 struct point 的结构体。结构体可以被初始化,方法是在其定义后面加上初始化表达式列表来为其成员初始化,每个初始化表达式都是一个常量表达式:
struct point maxpt = { 300, 200 };
自动结构体也可以通过赋值来初始化,或者通过调用返回一个该类型结构体的函数来初始化。
特定结构体的成员通过如下形式的表达式来引用
结构体名称 . 成员
结构体成员操作符 "." 连接了结构体名称和成员名称。例如,打印点 pt 的坐标:
printf("%d,%d", pt.x, pt.y);
或者计算原点 (0, 0) 到 pt 的距离:
double dist, sqrt(double);
dist = sqrt((double)pt.x * pt.x + (double) pt.y * pt.y);
结构体可以嵌套。矩形的一种表示法是用对角的两个点来表示:
struct rect {
struct point pt1;
struct point pt2;
};
rect 结构体包含两个 point 结构体。如果我们将 screen 定义为
struct rect screen;
则
screen.pt1.x
指的是 screen 成员 pt1 的 x 坐标。
6.2 结构体和函数
对结构体仅有的合法操作有:拷贝,作为一个整体对其赋值,用 & 对其取地址,以及访问其成员。拷贝和赋值包括将参数传递给函数,以及从函数返回值。结构体不能被比较。结构体可以用一个常量成员值的列表来初始化;自动结构体也可以通过赋值来初始化。
我们写几个函数来操作点和矩形,以此来研究结构体。至少有三种可能的方法:分别传各个部分,传整个结构体,或者传结构体的指针。每种方法都有其优缺点。
第一个函数 makepoint 接受两个整数参数并返回一个结构体:
/* makepoint:用 x 和 y 组成一个坐标 */
struct point makepoint(int x, int y)
{
struct point temp;
temp.x = x;
temp.y = y;
return temp;
}
注意同样的参数名称和成员名称不会造成冲突;实际上名字的重用还强调了它们之间的联系。
makepoint 现在可以用来动态地初始化任意结构体,或者用来为函数提供结构体参数:
struct rect screen;
struct point middle;
struct point makepoint(int, int);
screen.pt1 = makepoint(0, 0);
screen.pt1 = makepoint(XMAX, YMAX);
middle = makepoint((screen.pt1.x + screen.pt2,x)/2,
(screen.pt1.y + screen.pt2.y)/2);
下一步是一系列用来对点做算术运算的函数。例如
/* addpoint: 两个点相加 */
struct point addpoint(struct point p1, struct point p2)
{
p1.x += p2.x;
p1.y += p2.y;
return p1;
}
这里两个参数和返回值都是结构体。我们将 p1 中的成员递增,而不是创建一个临时变量,是为了强调结构体参数和其他参数一样,也是值传递的。
另一个例子,函数 ptinrect 检测一个点是否在一个矩形内部,我们采用了这样的约定:矩形包含了左边和底边,但不包含顶边和右边。
/* ptinrect: 如果p在r中则返回1,否则返回0 */
int ptinrect(struct point p, struct rect r)
{
return p.x >= r.pt1.x && p.x < r.pt2.x
&& p.y >= r.pt1.y && p.y < r.pt2.y;
}
这里假定矩形是以标准形式来表示的,即 pt1 的坐标小于 pt2 的坐标。下面的函数返回了一个保证是标准形式的矩形。
#define min(a, b) ((a) < (b) ? (a) : (b))
#define max(a, b) ((a) > (b) ? (a) : (b))
/* canonrect: 规范矩形坐标 */
struct rect canonrect(struct rect r)
{
struct rect temp;
temp.pt1.x = min(r.pt1.x, r.pt2.x);
temp.pt1.y = min(r.pt1.y, r.pt2.y);
temp.pt2.x = max(r.pt1.x, r.pt2.x);
temp.pt2.y = max(r.pt1.y, r.pt2.y);
return temp;
}
如果一个大结构体要传给函数,通常更高效的是传递一个指针而不是拷贝整个结构体。结构体指针和其他普通变量的指针一样。如下声明
struct point *pp;
表示 pp 是一个指向类型为 struct point 结构体的指针。如果 pp 指向一个 point 结构体,则 *pp 是结构体,而 (*pp).x 和 *(pp).y 是成员。例如,为了使用 pp ,我们可以写
struct point origin, *pp;
pp = &origin;
printf("origin is (%d,%d)\n", (*pp).x, (*pp).y);
(*pp).x 中的括号是必须的,因为结构体成员操作符 . 的优先级比 * 高。表达式 *pp.x 表示 *(pp.x),在这里是非法的,因为 x 不是一个指针。
指向结构体的指针使用是如此频繁,以至 C 语言提供了另一种表示法作为其简写方式。如果 p 是指向结构体的指针,则
p->结构体成员
指向特定的结构体成员。(操作符 -> 是减号后面立刻跟着大于号 >)。因此我们也能写成
printf("origin is (%d,%d)\n", pp->x, pp->y);
. 和 -> 都是从左往右结合的,因此如果我们有
struct rect r, *rp = &r;
则下面四个表达式是等价的:
r.pt1.x
rp->pt1.x
(r.pt1).x
(r->pt1).x
结构体操作符 . 和 ->,以及函数调用操作符(),还有下标操作符[ ],在运算符优先级中位于最顶部,因此绑定的非常紧密。例如,给出如下声明
struct {
int len;
char *str;
} *p;
则
++p->len
递增的是 len 而不是 p,因为隐含的括号是 ++(p->len)。可以使用括号来改变绑定关系:(++p)->len 在访问 len 之前对 p 递增,而 (p++)->len 在访问 len 之后对 p 递增。(后面的括号是不需要的。)
同样,*p->str 获取的是 str 指向的内容;*p->str++ 在访问 str 指向的内容之后再对 str 进行递增(类似 *s++); (*p->str)++ 是对 str 指向的内容进行递增;而 *p++->str 在访问str 指向的内容后对 p 进行递增。