itpt_TCPL 第五章:指针和数组 - 第八章:UNIX系统接口

来源:互联网 发布:数据库保护问题包括 编辑:程序博客网 时间:2024/04/30 03:07

2016.10.11 - 01.09
个人英文阅读练习笔记(极低水准)。

10.11

第五章:指针和数组

指针是保存一个变量的地址的变量。指针在C中被大量使用,部分是因为有时候它是唯一表达计算的方法,部分是因为和其它方式比起来它常能达到更佳的效果且效率更高。指针和数组紧密相关;本章也会探索二者的关系并介绍如何利用指针。

指针和goto语句成为了创造不太可能被理解的程序的主要语法。当它们被大意地使用时,的确如此,且很容易就让指针指向了一个并非所期望的地方。然而,按照规则使用指针,也能够达到清晰和简洁的效果。这也是我们尽量尝试演示指针使用的原因。

在ANSI C中的主要改变是对指针的操作做了清晰的规定,强制什么操作和编译对指针操作来说是高效的。另外,void 类型替换了原来的char 类型,作为通用指针。

1 指针和地址

用一张简化的内存是怎么被组织的图片作为本节内容的开始。一个典型的机器拥有能被单独或连续操作的连续标号(地址)的内存地址空间。一种常见的情形是一字节内存都可以被看作一个char,两个字节内存可被看作short integer,四个字节临近的内存单元形成long类型。一个指针是一个内存单元组(通常由2或4字节组成),该内存单元能够存储地址。所以,如果c是一个字符且p是指向c的指针,我们可以用下图表示它们的关系:
这里写图片描述
一元运算符 & 会给出对象的地址,所以语句

p = &c;

就将c的地址赋值给了p,这就是所听到的p指向c。& 只能作用于内存中的对象:变量和数组元素。它不能作用于表达式、常量以及寄存器(register)变量。

一元运算符 * 是间接或引用运算符;当它作用域一个指针时,它得到的是该指针所指向的对象。假设x和y是整型且ip是指向整型的指针。以下人为序列展示了如何声明一个指针并如何使用 & 和 *:

int x = 1, y = 2, z[10];int *ip;        /* ip 是一个指向int类型的指针 */ip = &x;        /* ip现在指向x */y = *ip;        /* y现在等于1 */*ip = 0;        /* x现在等于 0 */ip = &z[0]; /* ip现在指向z[0] */

x, y以及z是我们常见的一种声明方式。指针ip的声明

int *ip;

作为一种记忆;它表示*ip是一个整型。变量的声明酷似表达式中变量出现的语法。这些规则同样适用于函数声明。例如

double *dp, atof(char *);

表明表达式*dp和atof(s)拥有double类型的值,并且atof的参数是一个指向char的指针。

需要注意指针被约束于指向一种特殊类型的含义:每个指针指向一种特定的数据类型。(例外的是:指向void的指针,它可以用来指向任何类型但是不能引用一个void指针。我们将在5.11节讨论该话题)

如果ip指向整数x,那么*ip可以出现在x可以出现的任何地方,所以

*ip = *ip + 10;

让*ip增加了10。

一元运算符 * 和 & 比算术运算符的优先级要高,所以赋值表达式

y = *ip + 1

先将ip指向的数取出来,然后加1,再赋值给y,以及

*ip += 1

也是将ip指向的内容加1,就如

++*ip

(*ip)++

语句的作用。在后一个语句中,括号的作用是必要的;若无括号,表达式将为ip增1而非给ip指向的内容增1,因为像 * 和 ++ 这样的一元运算符的结核性是从右至左的。

最后,由于指针也是变量,它们在不引用的情况下也能被使用。例如,如果iq是指向int的另外一个指针,

iq = ip

就将ip的内容复制给iq了,这样也使iq指向了ip所指向的内容。

2 指针和函数参数

10.12
由于C给函数传参都是复制值的方式,所以在函数内没有直接的方法修改函数体外部的变量。例如,排序程序可能会用一个swap函数交换两个逆序元素的值。写成

swap(a, b);

这样是不够的。swap函数定义如下

void swap(int x, int y) /* 错误 */{    int temp;    temp = x;    x = y;    y = temp;}

由于参数通过传值方式,swap函数根本不能影响a和b的值。通过给调用函数传变量的地址的方式可以得到想要的效果:

swap(&a, &b);

因为操作符 & 是取变量的地址,所以 &a 是a的地址。对于swap函数自身来说,参数需要以地址的方式声明,在函数体内也要间接的访问到函数外的变量。

void (int *px, int *py)  /* 交换*px*py */{    int temp;    temp = *px;    *px = *py;    *py = temp;}

图示如下:
这里写图片描述

指针参数能够让函数访问并改变父函数内的变量对象。例如,将输入流中的数字字符串转换为整型值的getint函数,每次调用getint时,遇到其它的字符序列则返回。getint返回找到的数字,如果输入流中无更多输入时则返回文件结束信号。这些值应被独立的返回,不管使用EOF来做什么,也可以是整数的一个输入。

一种方法是让getint以函数返回值的方式返回文件结束状态,另一种方式是用一个指针参数指向存储被转换的整数的空间。这招被库函数scanf使用;见7.4节。

以下循环通过调用getint给数组元素赋值:

int n, array[SIZE], getint(int *);for (n = 0; n < SIZE && getint(&array[n]) != EOF; n++)    ;

每次调用都将找到的整数存储在数组array[n]中,同时将n增1。将array[n]的地址传给getint函数是必要的。否则,getint函数根本不能设置调用它的函数中的array[n]的值。

以下版本的getint函数在文件结束时返回EOF,在没有数字时返回0,如果输入流中含有效的数字则返回一个正数。

#include <ctype.h>int getch(void);void ungetch(int);/* getint: 将从输入流中获得的整数存储在*pn指向的内存中 */int getint(int *pn){    int c, sign;    while (isspace(c = getch())) /* 跳过空白字符 */        ;    if (!isdigit(c) && c != EOF && c != ‘+’ && c != ‘-‘) {        ungetch(c);        return 0;    }    sign = (c == ‘-‘) ? -1 : 1;    if (c == ‘+’ || c == ‘-‘)        c = getch();    for (*pn = 0; isdigit(c); c = getch())        *pn = 10 * *pn + (c – ‘0’);    *pn *= sign;    if (c != EOF)        ungetch(c);    return c;}

通过getint,*pn被当作一个普通的整型变量使用。该程序同时还使用了getch和ungetch(在4.3节),当额外的数据必须要被处理时就可以将它们返回到输入流中。

练习 5-1。以上所编写的getint,它并不处理+0或-0的情况。将这样的字符返回到输入流中修正该程序。
练习 5-2。编写程序getfloat,模拟getint函数以得到一个浮点型的数字。getfloat函数的返回值应该是什么类型?

3 指针和数组

10.13
在C语言中,指针和数组有及其强烈的联系,强烈到足够将指针和数组放在一起讨论。任何能以数组下标的操作都能用指针来完成。通常,指针还会更加的快速,但对于缺乏经验者来说是更难理解的。

声明

int a[10];

定义了一个大小为10个元素的整型数组,10个元组元素连续占用内存块,它们的名字依次为a[0], a[1], …, a[9]。
这里写图片描述

符号a[i]引用数组的第i个元素。如果pa是一个指向整型的指针,先作以下声明

int *pa;

赋值语句

pa = &a[0];

将pa的值设置为数组a的第0个元素的地址。
这里写图片描述

如下赋值语句

x = *pa;

将会把a[0]的值拷贝给x。

如果pa指向数组中的某个特定的元素,pa + 1就会指向下一个元素,pa + i会指向pa之后的第i个元素,pa - i会指向pa之前的第i个元素。因此,如果pa指向a[0],

*(pa + 1)

将引用a[1]的内容,pa + i是a[i]的地址,并且*(pa + i)是a[i]的内容。
这里写图片描述
无论类型或数组中元素大小,这些结论都正确。给指针加1的意思是让指针指向下一个元素。

索引和指针算术之间的关系比较近。根据定义,变量的值或数组类型表达式是数组第0个元素的地址。因此在赋值表达式

pa = &a[0];

之后,pa和a有相同的值。由于数组名和数组第一个元素的地址的值相同,赋值语句 pa = &a[0]也可以写为

pa = a;

其实,数组元素的引用a[i]也可以写为 (a + 1)。在对a[i]进行求值时,C也会将它转换为 (a + i)的形式;在C程序中,这两种写法是等效的。另外,&a[i]和a + i的值也是相同的:a + i从a起第i个元素的地址。从另一个角度讲,如果pa是一个指针,在表达式中pa也可以用下标的方式来表示;pa[i]跟*(pa + i)是相同的。简而言之,数组-索引的效果等效于指针加偏移的效果。

一定要注意数组名和指针的不同之处。指针是一个变量,所以pa = a以及pa++都是合法的。但是数组名不是变量;像a = pa以及 a++这样的语句是不合法的。

当数组名传递给函数时,实际上传递给函数的是首元素的地址。在子函数内,该参数是一个局部变量,所以函数的数组参数是一个指针,也就是说,它是一个保存地址的变量。我们可以利用这一点来编写另一个版本的strlen,该函数计算字符串的长度。

/* strlen: 返回字符串s的长度 */int strlen(char *s){    int n;    for (n = 0; *s != ‘\0’; s++)        n++;    return n;}

因为s是一个指针,对它执行自增操作是绝对合法的;s++对调用strlen函数内的字符串没有任何影响,s仅是strlen函数的一个私有指针变量。这就意味着,类似以下的调用

strlen(“hello, world”); /* 字符串常量 */strlen(array);          /* char array[100] */strlen(ptr);            /* char *ptr */

都是可行的。

就像函数定义中的正规参数一样,

char s[];

char *s;

都是一样的;我们更喜欢后者,因为后者更加明确了该参数是一个指针变量。当数组名传递给函数时,函数可以根据方便将该参数当作数组或指针使用,并分别对他们进行操作。甚至可以使用两种符号,如果这样可以让程序更加清晰和适合的话。

通过将数组某元素的首地址传递给给函数以叨叨传递数组的一部分的目的也是可行的。例如,假设a是一个数组,

f(&a[2]);

以及

f(a+2)

都可以将开始于a[2]的子数组传递给函数。在函数f中,参数声明可以是

f(int arr[]) {…}

或者

f(int *arr) {…}

就f而言,事实是参数引用更大数组的一部分是没有结果的。

如果已确定元素存在,也可以通过该参数回引数组元素;p[-1],p[-2]等也是合法的,这些引用不能超到p[0]之前。

4 地址运算

10.14
假设p指向拥有某些元素的数组,那么p++操作就会使得p指向下一个元素, p += i将会将p设置为当前p指向元素之后的i个元素的地址。这些都是指针或地址最简单的运算。

C用一致和有规律的方法来进行地址运算;它的指针集合,数组以及地址运算是该语言的一种优势。让我们编写一个基础的存储分配器来验证这一点。供有两段程序。第一个是alloc(n),返回指向n个连续字符内存空间的指针p,该空间可以在调用alloc后用来存储字符。第二个函数是afree(p),释放p指向的存储空间,这样p所指向的空间就可被再度使用。说该程序是基础的是因为调用afree必须要在调用alloc函数之后。也就是说,alloc和afree所管理的存储器是一个栈,即后进先出。标准库中提供的与该功能相似的malloc和free函数就没有这方面的限制;在8.7节我们将演示它们是如何被实现的。

最简单的实现是让alloc管理一个被名为allocbuf的较大的字符数组空间。该数组是alloc和afree的私有变量。由于不采取数组索引而采取指针的方式操作,其它的程序不会知道数组名,因为该数组在alloc和afree所在的源文件中被声明为static类型,对其它源文件不可见。在实际的实现中,数组甚至连名字也没有;它可能通过调用malloc或通过操作系统请求而得到一个指向一个未命名的空间的指针。

还需要知道allocbuf中已经被使用了多少空间的信息。我们使用一个名为allocp的指针来完成该功能,该指针指向下一个空闲的元素。当调用alloc来请求n字节的空间时,该函数会首先检查在allocbuf中是否有足够的空间。如果有,alloc放回allocp当前值(例如,空间空间的开始地址),然后将allocp增加n让其指向下一段空闲的空闲区域。如果无足够的空间,alloc则返回0。如果p在allocbuf中,afree(p)仅仅设置allocp指向p。
这里写图片描述

#define ALLOCSIZE       10000 /* 可用空间的尺寸 */static  char    allocbuf[ALLOCSIZE];    /* 给alloc分配使用的存储空间 */static  *allocp = allocbuf;         /* 下一个空闲空间 */char    *alloc(int n)   /* 返回指向n个字符的指针 */{    if (allocbuf + ALLOCSIZE - allocp >= n) {   /* 满足 */        allocp  += n;        return allocp - n;  /* 旧p */    } else  /* 空间不足 */        return 0;}void afree(char *p)     /* 由p指向的空间 */{    if (p >= allocbuf && p < allocbuf + ALLOCSIZE)        allocp  = p;}

通常来讲,指针可以像其它变量那样被初始化,尽管它的初始化部分要么是0要么是一个包含该指针指向类型的地址。声明

static  char *allocp = allocbuf;

定义allocp为一个字符指针并将它初始化为allocbuf,当程序开始时它是allocbuf的一段空闲空间。这个语句也可以写成

static  char *allocp = &allocbuf[0];

因为数组名就是第0个元素的地址。

测试条件

if (allocbuf + ALLOCSIZE - allocp >= n) { /*满足 */

检查是否有满足请求n个字符的空闲空间。如果有,allocp的值最多被更新到超出allocbuf一个元素。如果请求满足,alloc将会返回一个指向n字节空间的首地址的指针(注意该函数的声明)。如果不满足,alloc必须返回信号来表示当前没有足够的空间。C保证0不会是一个有效的地址,所以alloc返回0能够用作表示一个反常的事件,在这里就表示没有足够的空间可用。

指针和整数是不可互换的。0是唯一的例外:0可以赋值给指针,指针也可以和常量0作比较。常量NULL常用赖代替0,作为一种更清晰地用来表示特殊指针值的方式。NULL定义在stddef.h中,尽管它也出现在诸如stdio.h和stdlib.h这样的其它头文件中。今后我们将使用NULL。

测试条件

if (allocbuf + ALLOCSIZE - allocp >= n) { /* 满足 */

以及

if (p >= allocbuf && p < allocbuf + ALLOCSIZE)

展示了指针运算重要的几个方面。首先,指针在特定的环境下是可以作比较的。如果p和q分别指向相同数组的某个元素,那么像==, !=, <, >=,等这些比较都是合适的。例如

p < q

为真,如果p指向的元素在q所指元素之前。任何指针都可以用来和0作相等或不等的比较。对于指向不同内存段(如不同数组)的两个指针做算术运算或比较是没有定义的。(一个例外是:数组最后一个元素后面的那个地址在指针算术运算中可用)

其次,我们已经看到指针和整数可以进行加减操作。以下形式

p + n

表示p所指向元素起后数第n个元素的地址。无论p所指向的对象的类型,这一点都是成立的;基于p所指向的具体类型n是可以缩放的,它依据n的声明。假设int占用4字节,那么n的缩放倍数就是4。

指针减法同样也是有效的:如果p和q指向同一个数组且p < q,那么q - p + 1的值就是p和q所指元素之间的元素个数。利用这一点就可以编写另一个版本的strlen:

/* strlen: 返回字符串s的长度 */int strlen(char *s){    char *p = s;    while (*p != ‘\0’)        p++;    return p - s;}

在它的声明中,p被初始化为s,也就是说,p指向字符串的第一个字符。在while循环中,每个字符都会被数到,直到字符串的结束标记为止(不包括结束标记)。因为p指向字符数组,p++就会指向下一个字符,p - s 就得到了p和s之间的字符个数,也就是字符数组的长度。(字符串的长度有可能大道一个int类型都存储不下。头文件stddef.h定义了类型ptrdiff_t,它足够大,可以保存两个不同指针之间的差值。然而,我们需要十分谨慎,我们使用size_t作为函数strlen的返回值,以和标准库中的版本一致。size_t是一种由sizeof运算符返回的无符号整数类型)

指针运算是一致的: 如果我们处理比字符类型占用更多空间的浮点数,且p指向浮点型,p++就将会指向下一个浮点数。因此我们可以编写一个管理浮点数内存空间分配的alloc和afree。所有的指针运算都会自动根据对象所占用的尺寸而进行相应的运算。

有效的指针运算包括:相同类型指针之间赋值、指针加或减一个整数、对指向同一个数组的两个指针进行减或比较、为指针赋值0或指针和0相比较。除这些运算外,其它的指针运算都是非法的。两个指针相加、相乘或指针的移位或掩位操作或给指针加浮点或double类型的书甚至给一个中类型的指针赋值另外一种类型的指针(void *除外)都是不合法的。

5 字符指针和函数

10.15
在C程序中像这样的表达

"I am a string"

就是一个字符串常量,它是一个字符的数组。在内部存储上来讲,该数组由空字符’\0’结束,所以程序能够找到数组的末尾。所以实际需要的存储空间要比引号引起来的字符个数多1。
也许最常见的字符串常量是作为参数传递给一个函数,如

printf(“hello, world\n”);

当一个字符串像这样出现在程序中时,都是通过字符指针来访问该字符串;printf函数接收到一个指向字符数组开始处的指针。也就是说,字符串常量的访问是通过指向该字符首字符的指针来完成的。

字符串常量不必直接成为函数实参。假设pmessage的声明如下

char    *pmessage;

然后赋值语句

pmessage    = “now is the time”;

就将pmessage指向字符数组的首元素。这并不是一个字符串的拷贝;只是对一个指针的赋值。C没有提供将字符串当作一个单元的任何操作符。

以下两个定义间有重大区别:

char    amessage[] = “now is the time”; /* 数组 */char *pmessage = “now is the time”;  /*指针 */

amessage是一个数组,刚好能够容纳初始化它的字符串(包含结束标志’\0’)。在数组内的每个字符都可以被改变,对其内每个元素的访问都是操作的数组中的存储空间。而pmessage是一个指针,它指向一个字符串常量;该指针可以被更改指向其他的内存空间,但若通过该指针去更改字符串的内容则结果是未经定义的。
这里写图片描述
我们将通过来自标准库的两个函数来继续说明指针和数组的更多方面。第一个函数是strcpy(s, t),该函数将字符串t拷贝至s处。如果只采用s = t的方式看起来十分简洁,但这只是一个指针拷贝,并没有拷贝字符。欲拷贝字符串,我们需要用循环来实现。先用数组来实现第一个版本:

/* strcpy:复制t到s;数组下标版本 */void strcpy(char *s, char *t){    int i;    i   = 0;    while ((s[i] = t[i]) != ‘\0’)         i++;}

作为比较,以下版本通过指针实现:

/* strcpy: 复制t到s,指针版本1 */void strcpy(char *s, char *t){    while ((*s = *t) != ‘\0’) {        s++;        t++;    }}

由于参数都是通过值传递,strcpy可以任何方式使用参数s和t。这里是直接使用它,每次都用它拷贝一个字符,直到字符串结束标志’\0’为止。

实际上,strcpy不应像以上版本那么写。经验丰富一点的程序员会喜欢以下版本的strcpy:

/* strcpy: 拷贝t到s;指针版本2 */void strcpy(char *s, char *t){    while((*s++ = *t++) != ‘\0’)        ;}

该版本直接将s和t放到循环测试部分。*t++的值是t增加之前所在的字符;后缀++直到字符被访问后才会对t起作用。同理,字符存储在s增加之前的旧地址s中。该字符和’\0’作为循环测试条件。最后的结果是将t中所有的字符都拷贝到s中了,连’\0’字符也拷贝进去了。

最后再观察一下,while条件中和’\0’的比较其实是多余的,因为只要表达式为0 while循环就会终止,而’\0’本身就是0。所以该函数可以写成如下形式

/* strcpy:拷贝t到s;指针版本3 */void strcpy(char *s, char *t){    while (*s++ = *t++)        ;}

尽管该版本初看起来颇有些神秘,但未方便起见,这些都相当于属于,它应该被掌握,因为在C程序中这样的编写方式是常见的表达方式。

第二个程序是strcmp(s, t),它比较字符串s和t,如果s小于t则返回一个负数,若s和t相等则返回0,若s大于t则返回一个正数。返回值由s和t字符串中两个字符的差值得来。

/* strcmp:如果s < t返回负数,如果s 等于t则返回0,否则返回正数 */int strcmp(char *s, char *t){    int i;    for (i = 0; s[i] == t[i]; i++)        if (s[i] == ‘\0’)            return 0;    return s[i] – t[i];}

strcmp的指针版本为:

/* strcmp:如果s < t返回负数,如果s 等于t则返回0,否则返回正数 */int strcmp(char *s, char *t){    for ( ; *s == *t; s++, t++)        if (*s == ‘\0’)            return 0;    return *s - *t;}

++和– 是前缀和后缀运算符,它们会和*结合使用。例如,

*--p

将在取p所指向内容之前将p自减。实际上,表达式对

*p++ = val; /* 将val压入栈中 */val = *--p;     /* 出栈到val */

是进栈和出栈常用的语句;见4.3节。
头文件string.h包含了在本节中所提到的函数,在该头文件中还声明了其它字符串相关的函数。

练习 5-3。用指针编写第二章编写的将t拷贝到s末尾的strcat(s, t)函数。
练习 5-4。编写函数strend(s, t),如果字符串t出现在字符串s的末尾就返回1,否则返回0。
练习 5-5。重新编写库函数中的strncpy、strncat以及strncmp,它们最多操作参数中n个字符。例如,strncpy(s, t, n)最多复制t的n个字符到s。详细的描述在附录B中。
练习 5-6。用指针重新编写之前章节用数组索引编写的练习。如getline(第一章和第四章所涉及),atoi,itoa以及它们的变体(第2,3,4章),reverse(第三章),以及strindex和getop(第四章)。

6 指针数组;指向指针的指针

10.17
由于指针本身也是变量,所以它们也可以像其它变量那样存储在一个数组里。让我们编写将文本行以字母顺序排序的程序,即编写一个UNIX系统中sort的简洁版本。

第三章呈现了一个用来排序整型数组的Shell sort函数,在第四章我们将它提升为了快速排序。这些算法也能够用于排序文本行,只是不同的文本行拥有不同的长度且不像整数那样可以用 == 直接比较。我们需要一个用来表示将高效和方便处理文本行长度的数据。

指针数组就可以被排上用场了。如果文本行被一对一的存储在一个长数组中,通过一个指向每一行首字母的指针就可以访问该行。指针本身就可以存储在一个数组里。通过将指向它们的指针传递给strcmp就可以比较两个不同的行。当两个行逆序时,就可以只交换两个指针,而不用交换两行。
这里写图片描述

这可以消除需要移动字符串本身的复杂的内存管理以及高消耗两个问题。
该排序需处理三个步骤:

读取所有的输入行排序行顺序打印输入行

通常,最好是将程序分割成与以上划分步骤的三个函数,然后再用主函数来控制这些子函数。我们将排序程序推迟,现在主要将精力集中在数据结构和输入输出上。

输入程序需要收集和保存输入行,并构建指针数组来指向这些行。该程序同时还需要对输入行进行计数,因为输入行数在排序和打印时会被用到。由于输入函数只能处理有限的输入行,所以它可以通过返回诸如-1来表示输入行超标。

输出程序只需要打印已经排好序的文本行指针。

#include <stdio.h>#include <string.h>#define MAXLINE 5000        /* 排序的最大行数 */char    *lineptr[MAXLINE];      /* 指向文本行 */int readlines(char *lineptr[], int nlines);void writelines(char *lineptr[], int nlines);void qsort(char *lineptr[], int left, int right);/* 排序输入行 */main(){    int nlines;     /* 读入的输入行的行数 */    if ((nlines = readlines(lineptr, MAXLINES)) >= 0) {        qsort(lineptr, 0, nlines – 1);        writelines(lineptr, nlines);        return 0;    } else {        printf(“error: input too big to sort\n”);        return 1;    }}#define MAXLEN      1000    /* 输入行的最大长度 */int getline(char *, int);char *alloc(int);/* readlines: 读取输入行 */int readlines(char *lineptr[], int maxlines){    int len, nlines;    char *p, line[MAXLINE];    nlines  = 0;    while ((len = getline(line, MAXLINE)) > 0)        if (nlines >= maxlines || (p = alloc(len)) == NULL)            return -1;        else {            line[len – 1] = ‘\0’;   /* 删除换行符 */            strcpy(p, line);            lineptr[nlines++]   = p;        }    return nlines;}/* writelines:输出行 */void writelines(char *lineptr[], int nlines){    int i;    for (i = 0; i < nlines; i++)        printf(“%s”, lineptr[i]);}

函数getline来自1.9节。

该函数中主要的新知识点是lineptr的声明:

char    *lineptr[MAXLINE];

该声明表示lineptr是一个拥有MAXLINE元素的数组,每个元素都是一个指向char的指针。也就是说,lineptr[i]是一个字符指针,且*lineptr[i]是所指向的第i-th行的首字符。

因为lineptr本身是数组名,它也可以像以前提到的那样被当成一个指针,所以writelines也可以被写成如下方式

/* writelines:输出行 */void writelines(char *lineptr[], int nlines){    while (nlines-- > 0)        printf(“%s\n”, *lineptr++);}

最开始的*lineptr指向第一个文本行;每次自增操作都将下一个指针元素指向下一个文本行。

在控制了输入输出的情况下,我们就可以处理排序了。需要对第四章的快速排序进行一些改变:声明需要被改变、比较操作必须通过调用strcmp来完成。算法还是相同的。

/* qsort:增序排序v[left]…v[right] */void qsort(char *v[], int left, int right){    int i, last;    void swap(char *v[], int i, int j);    if (left >= right)  /* 如果被分割的数组包含的元素小于2 */        return ;    swap(v, left, (left + right) / 2);    last    = left;    for (i = left + 1; i < right; i++)        if (strcmp(v[i], v[left]) < 0)            swap(v, ++last, i);    swap(v, left, last);    qsort(v, left, last - 1);    qsort(v, last + 1, right);}

同理,swap程序也需要一些琐碎的改变:

/* swap:交换v[i]和v[j] */void swap(char *v[], int i, int j){    char *temp;    temp    = v[i];    v[i]        = v[j];    v[j]        = temp;}

因为v的每个元素也是一个字符指针,所以temp也必须是一个字符指针,指针之间是可以相互赋值的。

练习 5-7。重新编写readlines将文本行存储在main函数中的数组中,而不是通过alloc来分配内存。该程序会比源程序快多少?

7 多维数组

10.19
C提供了矩阵多维数组,尽管它们在实际中比指针数组用得少。在本节,我们将展示多维数组的特性。

思考日期转换问题,从某月某天转换到改天是该年的多少天,反之亦然。例如,三月1是平年的第60天;是闰年的第61天。让我们定义两个函数来完整它们之间的转换:day_of_year将月和天转换为天中的第几天,monty_day将一年中的天转换到月和天。因为后者会计算两个值,月和天参数都应是指针:

month_day(1988, 60, &m, &d);

将m设置为2且将d设置为29(2月29日)。

这些函数需要相同的信息,每个月中的天数表。因为每月的天数会依据是否为闰年而不同,所以最好是将他们对应的天数放到二维数组中。数组和执行换行的函数如下:

static char daytab[2][13] = {    {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},    {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};/* day_of_year:计算几月几号在一年中的天数 */int day_of_year(int year, int month, int day){    int i, leap;    leap    = year % 4 == 0 & year % 100 != 0 || year % 400 == 0;    for (i = 1; i < month; i++)        day += daytab[leap][i];    return day;}/* month_day:根据一年中的第几天设置成某月某号 */void month_day(int year, int yearday, int *pmonth, int *pday){    int i, leap;    leap    = year % 4 == 0 && year % 100 != 0 || year % 400 == 0;    for (i = 0; yearday > daytab[leap][i]; i++)        yearday -= daytab[leap][i];    *pmonth = i;    *pday   = yearday;}

重新调用逻辑表达式的算术值,诸如leap,它的值不是0就是1,所以它可以作为daytab的下标。数组daytab必须是day_of_year和month_day的外部变量,这样二函数才可使用它。我们演示了使用char来存储非数字整数的合法性。

daytab是我们使用的第一个两维数组。在C中,两位数组其实是一个一维数组,它的每个元素是一个数组。因此引用下标

daytab[i][j] /* [行][列] */

而不是

daytab[i, j] /* 错误 */

除需注意这一点符号区别外,也可按照相同方式对待其它语言中的二维数组。元素是按行存储,所以最右或叫作列下标,是访问存储时变化得最快的变量。

数组是被包含在大括号中的初始化部分初始化的;二维数组的每一行由相应子列表初始化。我们将daytab的列以0开始,这样月份就可以从1到12而非从0到11。这里添加的0元素空间并不算一个高昂的代价,反而使程序更加的清晰化了。

如果二维数组被传递给一个函数,函数中的形参必须包含二维数组的列;行数可以被省略,因为跟以前描述的一样,传递给函数的是一个指向一维数组的指针,该数组个元素是一个数组。在该特例下,它是一个指向13个整型的指针。因此如果数组daytab被传递给函数f,需要像以下这样声明f

f(int daytab[2][13]) {…}

也可以声明为

f(int daytab[][13]) {…}

因为行数可省略的,所以也可以被声明为

f(int (*daytab)[13]) {…}

该声明表示参数是一个指向包含13个整数数组的指针。括号是必须的,因为[]的优先级比*高,如果没有括号,声明

int *daytab[13]

是13个指向整数的指针。更通用的说,只有数组的第一维下标是可以省略的,其它维的下标都必须指定。
练习 5-8。在day_of_year或month_day中错误检查功能。改进该点。

8 指针数组的初始化

思考编写函数month_name(n),该函数返回指向包含n-th月份名字符串的指针。这是内部static数组的典型应用。month_name包含私有的字符串数组,并返回指向相应一个字符串的指针。此节展示如何初始化数组。

该语法类似之前的初始化:

/* month_name:返回n-th月份名 */char *month_name(int n){    static char *name[] = {        “Illegal month”,        “January, “February”, “March”,        “April”, “May”, “June”,        “July”, “August”, “September”,        “October”, “November”, “December”    };    return (n < 1 || n > 12) ? name[0] : name[n];}

所声明的name其实是一个字符指针数组,它跟存储例子中的lineptr相同。初始化部分是字符串列表;每个都被分配到数组的相应位置中。i-th字符串中的字符被存储某个地方,通过name[i]被指针指向。因为数组name的大小并未指定,所以编译器会根据初始化部分并计算出name相应的大小。

9 指针数组 vs 多维数组

10.20
一些C初学者会对二维数组和指针数组的区别感到迷惑,诸如之前例子中的name。给出的定义

int a[10][20];int *b[10];

a[3][4]和b[3][4]都是指向一个int的合语法的引用。但是a是一个二维数组:它会被分配200个int大小的空间,且可以将它看成是一个用20 x 行 + 列就可以引用到元素a[行][列]的矩阵。然而,对于b来说,它占用10个未初始化的指针大小的空间;必须对b进行明确的初始化,不管是在用静态还是用代码的方法。假设b的每一个元素指向一个20元素的数组,然后就应分配200个整形空间,再加上10个指针的空间。指针数组的优势在于数组的行可以是不同的长度。也就是说,b的每一个元素都不必指向大小为20元素的适量;一些可以指向拥有两个元素的,一些可以指向拥有50个元素的,一些也可以指向空。

尽管我们已经就整型为例简要讨论了指针数组,但目前指针数组被用得最多的还是用来指向不同长度的字符串,就像month_name中那样。比较指针数组和多维数组的声明与图示:

char        *name[] = { “Illegal month”, “Jan”, “Feb”, “Mar” };

这里写图片描述

再看二维数组:

char        aname[][15] = { “Illegal month”, “Jan”, “Feb”, “Mar” };

这里写图片描述

练习 5-9。用指针代替索引重新编写day_of_year和month_day。

10 命令行参数

10.21
在支持C的环境中,在执行C程序时可以接收命令行参数。当main函数被调用时,它可以被传递两个函数。第一个参数(通常称为argc,用来数命令行参数的个数)是传递给C程序的命令行参数个数;第二个参数(通常称为argv,用于指向命令行参数)是指向包含参数的字符串数组的指针,每一个参数都是字符串。我们通常使用多维指针来操作这些字符串。

最简单的例子程序是echo,它将它的命令行参数输出到一行中,每个参数由空格隔开。也就是说,命令

echo hello, world

将会打印

hello, world

通过转换,argv[0]是执行程序的程序名,所以argc的值至少为1。如果argc为1,就说明在可执行程序名后没有再跟其他的参数。在上例中,argc为3,且argv[0],argv[1],argv[2]分别为”echo”,”hello,”,”world”。第一个可选参数是argv[1],最后一个是argv[argc - 1];另外,标准需要argv[argc]为空指针。
这里写图片描述

第一个版本的echo将argv当作字符指针数组:

#include <stdio.h>/* 输出命令行参数;第一个版本 */main(int argc, char *argv[]){    int i;    for (i = 1; i < argc; i++)        printf(“%s%s”, argv[i], (i < argc - 1) ? “ “ : “”);    printf(“\n”);    return 0;}

因为argv是指向指针数组的指针,我们就可以不通过索引而直接操纵指针。下一个版本就自增argv,即一个指向指向字符的指针的指针,用argc来数数:

#include <stdio.h>/* 输出命令行参数;版本2 */main(int argc, char *argv[]){    while (--argc > 0)        printf(“%s%s”, *++argv, (argc > 1) ? “ “ : “”);    printf(“\n”);    return 0;}

因为argv是指向参数字符串数组开始的指针,增1(++argv)将让指针变为argv[1]而不再是argv[0]。每个连续的自增将会让那个argv[0]指向下一个参数;*argv就指向了该参数。同时,argc自减;当它变为0时,就没有可供打印的参数了。

另外,我们可以像如下语句写printf语句:

printf((argc > 1) ? “%s “ : “%s”, *++argv);

该语句表示printf的参数也可以是表达式。

作为第二个例子,让我们增强一下来自4.1节的模式-搜索程序。如果重新调用,我们将添加搜索模式到程序中,这明显不符合安排。根据UNIX程序grep,让我们改变程序以让模式匹配由第一个命令行参数指定的字符串。

#include <stdio.h>#include <string.h>#define MAXLINE 1000int getline(char *line, int max);/* find:打印匹配第一个命令行参数的行 */main(int argc, char *argv[]){    char line[MAXLINE];    int found = 0;    if (argc != 2)        printf(“Usage: find pattern\n”);    else        while (getline(line, MAXLINE)) > 0)            if (strstr(line, argv[1])) != NULL) {                printf(“%s”, line);                found++;            }    return found;}

10.22
标准库函数strstr(s, t)返回字符串t在字符串s中首次出现为止的指针,如果t不在s中,则返回NULL。该函数被声明在string.h中。

该结构可以进一步用来阐述指针。假设我们想允许两个可选参数。一个用来表示“打印除匹配模式的所有语句”;另一个用来表示“根据行号打印行”。

UNIX系统上的C程序的常用一个符号引进可选标记或参数。如果我们选择 -x(用来表示“除”的意思)转换,用-n来表示行排序,那么命令

find -x -n pattern

将会按照行号顺序打印不匹配模式的行。

可选参数应可以任何顺序出现,剩余程序应独立于我们所提供的参数。而且,如果参数可以被联合使用对使用者来说也是方便的,如

find    -nx pattern

以下是程序部分:

#include <stdio.h>#include <string.h>#define MAXLIEN 1000int getline(char *line, int max);/* find:打印匹配第一个模式参数的所有行 */main(int argc, char *argv[]){    char line[MAXLINE];    long    lineno = 0;    int c, except = 0, number = 0, found = 0;    while (--argc > 0 && (*++argv)[0] == ‘-‘)        while (c = *++argv[0])            switch (c) {            case ‘x’:                except  = 1;                break;            case ‘n’:                number  = 1;                break;            default:                printf(“find: illegal option %c\n”, c);                argc = 0;                found   = -1;                break;            }    if (argc != 1)        printf(“Usage: find -x -n pattern\n”);    else        while (getline(line, MAXLINE) > 0) {            lineno++;            if ((strstr(line, *argv) != NULL) != except) {                if (number)                    printf(“%ld:”, lineno);                printf(“%s”, line);                found++;            }        }    return found;}

在访问每个可选参数前argc自减,argv自增。在循环末尾,如果没有错误出现,argc将会告知有多少个参数还未被处理,argv指向这些未处理参数的第一个。因此argc应该为1且argv指向模式参数。注意,++argv是指向参数字符串的指针,所以(++argv)[0]是该字符串的第一个字符。(另一种可选的有效格式为*++argv)因为[]的优先级比和++更高,所以括号是必要的;如果没有括号,那么表达式就相当于++(argv[0])。事实上,我们在内循环中所使用的,每次都会跨过一个字符串即到达下一个字符串处。在内循环中,表达式*++argv[0]将把指针argv[0]增1。

比以上指针表达式更复杂的情况很少见;若出现这种情况下,将她们分成两到三部分会更直观。

练习 5-10。编写程序expr,它将从命令行求逆Polish表达式,在命令行中的表达式中,每个操作符及每个操作数都是被空格分开的。例如

expr        2   3   4   +   *

被求值为2 x (3 + 4)。

练习 5-11。修改entab和detab程序(在第一章的练习中)接受一列tab停止位为参数。如果没有参数则使用默认的tab设置。
练习 5-12。扩展entab和detab接受

entab   -m  +n

并将其含义解释为tab键从m列开始每n列作为停止位置。为用户选择方便的默认行为。

练习 5-13。编写程序tail,该程序输入行的最后n行。默认情况下,n为10,但它可以根据命令行参数被改变,所以

tail    -n

打印最后n行。该程序不管输入多么不合理以及n的值为多少都应该要给出一个合理的反映。编写该程序且要尽量合理的使用存储;每行内容应像5.6节的排序程序那样存储,不应该存储在一个固定大小的二维数组中。

11 函数指针

10.23
在C中,函数本身不是变量,但可以定义指向函数的指针,该指针可以被赋值,可作为数组元素,可以被传递给函数,被函数返回等等。我们通过修改本章之前所写的排序程序来演示这一点,当给定可选参数-n时,输入行就按照数字排序而不再按照字母顺序排序。

排序通常由三部分组成 —— 比较两个对象以判断它们的顺序,交换逆序的对象,以及安排比较和交换的算法直至所有元素有序。排序算法独立于比较和交换操作,所以通过传递给排序算法不同的比较和交换函数,就可以按照不同的标准进行排序。该方法在新排序方法中被采用。

之前,两行以字母顺序比较由strcmp函数完成;这里需要一个依照比较数字来排序的函数numcmp,它的返回值类似strcmp。这些函数以及含指向相应函数指针参数的qsort函数被声明在main函数之前。这里省去了参数的错误处理,所以就可以集中精力到main函数上。

#include <stdio.h>#include <string.h>#define MAXLINE 5000    /* 被排序的行数最大值 */char        *lineptr[MAXLINE];  /* 指向下一行的指针 */int readlines(char *lineptr[], int nlines);void writelines(char *lineptr[], int nlines);void qsort(void *lineptr[], int left, int right, int (*comp)(void *, void *));int numcmp(char *, char *);/* 对输入行排序 */main(int argc, char *argv[]){    int nlines;         /* 输入行行数 */    int numeric = 0;    /* 如以数字排序则为1 */    if (argc > 1 && strcmp(argv[1], “-n”) == 0)        numeric = 1;    if ((nlines = readlines(lineptr, MAXLINES)) >= 0) {        qsort((void **)lineptr, 0, nlines – 1, (int(*)(void *, void *))(numeric ? numcmp : strcmp));        writelines(lineptr, nlines);        return 0;    } else {        printf(“input too big to sort\n”);        return 1;    }}

在qsort中,strcmp和numcmp是其函数的地址。因为它们俩都为函数,所以&操作符就没有必要了,同理数组名前也不需要&操作符。

我们已经编写了可以处理任何数据类型的qsort程序。正如由函数原型所表明的那样,qsort期盼一个指针数组,两个整数,以及含两个指针参数的函数。通用指针类型void 用来作为指针的参数。任何指针都可以和void 类型相互转换,不会丢失任何信息,所以我们可以在调用qsort时可以将参数转换为void *。函数参数将会和比较函数之间进行转换。这通常并没有任何实际的效果,但要保证编译器没有报错。

/* qsort:以增续排v[left]…v[right] */void    qsort(void *v[], int left, int right, int (*comp)(void *, void *)){    int i, last;    void swap(void *v[], int, int);    if (left >= right)  /* 如果分割来的数组少于两个元素则什么也不做 */        return ;    swap(v, left, (left + right) / 2);    last    = left;    for (i = left + 1; i <= right; i++)        if ((*comp)(v[i], v[left]) < 0)            swap(v, ++last, i);    swap(v, left, last);    qsort(v, left, last – 1, comp);    qsort(v, last + 1, right, comp);}

10.24
应细心注意这些声明。qsort的第四个参数为

int (*comp)(void *, void *)

该声明表示comp是一个指向函数的指针,该函数拥有两个void *类型参数且返回一个int。

comp的使用在

if ((*comp)(v[i], v[left]) < 0)

它与声明一致:comp是一个指向函数的指针,*comp就是该函数且

(*comp)(v[i], v[left])

就是调用comp所指向的函数。括号是必须的,这样才能保证每部分被正确关联;如果没有括号

int *comp(void *, void *) /* 错误 */

就表明comp是一个返回int的函数,它跟有括号的情况很不相同。

strcmp已经被展示过了,它的作用是用来比较两个字符串。这里的numcmp,它将字符串当作数字来比较,字符串转换为数字由atof完成:

#include <stdlib.h>/* numcmp:以数字形式比较s1和s2 */int numcmp(char *s1, char *s2){    double v1, v2;    v1  = atof(s1);    v2  = atof(s2);    if (v1 < v2)        return -1;    else if (v1 > v2)        return 1;    else        return 0;}

swap函数交换两个指针,除了声明外,它跟前一章所呈现的相同。

void    swap(void *v[], int i, int j){    void *temp;    temp    = v[i];    v[i]    = v[j];    v[j]    = temp;}

添加一些可选的变化到排序程序中;以下是一些具有挑战性的练习。

练习 5-14。修改排序程序处理 -r 参数,该参数表示以逆序排序。确保 -r 能和 -n 连用。
练习 5-15。增加 -f 参数来屏蔽字母大小写情况;比如,a和A被视为相等。
练习 5-16。增加 -d (目录顺序)选项,该选项表示只在字母、数字或空格之间作比较。确保该参数能和 -f 连用。
练习 5-17。增加域处理功能(field-handing capability),让排序可以在行的域内进行,每个域独立于一套选项。(本书的索引使用-df作为索引目录的排序,用-n作为页码排序。)

12 复杂声明

10.25
C的声明语法有时会受到严厉指责,尤其是涉及函数指针的部分。声明是为了让声明和使用形式保持一致;对于简单情况情况,该机制能工作良好,但对于比较复杂的情况往往还会令人困惑难解,因为声明不是从左到右阅读的,因为括号会被大量使用。在

int *f();   /* f: 返回指向int的指针的函数 */

int (*pf)();    /* pf: 指向返回int的函数的指针 */

演示了问题:*是前缀操作符,它的优先级比()低,所以为了强制达到合适的关联,括号是必须的。

尽管复杂的声明会少在实际中被使用,但知道如何理解它们还是很重要,如果有必要,还要懂得如何创建这些复杂的声明。一个比较好的方法是通过使用typedef来一步一步合成复杂声明,这将会在6.7节被讨论。本节将会介绍将有效的C声明转换为语句描述和将描述C声明的语句转换为C声明的程序对。得到的描述语句从左向右阅读。

dcl是一个复杂的程序。它用语言描述一个C声明,如下例子中:

char    **argv    argv:指向指向字符的指针的指针int (*daytab)[13]    daytab:指向维数为13的整型数组的指针int *daytab[13]    daytab:维数为13的整型指针数组void    *comp()    comp:返回void *指针的函数void    (*comp)()    comp:指向返回void类型的函数的指针char    (*(*x())[])()    x:一个返回 指向 指向 返回字符的函数 的指针数组 的函数char    (*(*x[3])())[5]    x:一个维数为3的指向 返回指向 维数为5的数组的指针 的函数 的数组

dcl基于指定声明的语法,该语法的细节在附录A和8.5节中被涉及;以下是其被简化的格式:

dcl:    可选的*的是一个direct-dcl    直接的dcl: name            (dcl)            direct-dcl()            direct-dcl[oprional size]

总之,dcl是一个direct-dcl,在*的前面。direct-dcl是一个名字或括起来的dcl,或是一个跟随direct-dcl的括号,或direct-dcl跟随着可选大小的中括号。

该语法被用来解析声明。例如,以下声明:

(*pfa[])()

pfa将会被当作一个name,所以它是一个direct-dcl。然后pfa[]也成了一个direct-dcl。然后*pfa[]会被当作一个dcl,所以(*pfa[])是一个direct-dcl。然后(*pfa[])()是direct-dcl并成为dcl。我们也可以通过词法树来验证该点(direct-dcl被缩写为dir-dcl):
这里写图片描述
dcl程序的核心是一对函数,dcl和dirdcl,这两个函数根据以上提到的语法解析声明。因为该语法是递归定义的,所以当解析声明时俩函数彼此相互递归调用;该程序被称之为递归下降解析。

/* dcl:解析声明 */void dcl(void){    int ns;    for (ns = 0; gettoken() == ‘*’;)    /* 数*’s */        ns++;    dirdcl();    while (ns-- > 0)        strcat(out, “pointer to”);}/* dirdcl:解析直接声明符 */void dirdcl(void){    int type;    if (tokentype == ‘(‘) { /* (dcl) */        dcl();        if (tokentype != ‘)’)            printf(“error: missing)\n”);    } else if (tokentype == NAME)       /* 变量名 */        strcpy(name, token);    else        printf(“error: expected name or (dcl) \n”);    while ((type = gettoken()) == PARENS || type == BRACKETS)        if (type == PARENS)            strcat(out, “ function returning”);        else {            strcat(out, “ array”);            strcat(out, token);            strcat(out, “ of);        }}

10.27
因为程序的意图是演示,并不是实用的,所以dcl程序有很多限制。它只能处理像char或int这样简单的数据类型。它不能处理函数中的参数类型或像const这样的修饰符。多余的空格也会给dcl程序带来干扰。dcl也不能处理错误,所以无效的声明也会干扰它。这些提升都余下作为练习。

以下是全局变量和主程序:

#include <stdio.h>#include <string.h>#include <ctype.h>#define MAXTOKEN    100enum {NAME, PARENS, BRACKETS };void dcl(void);void dirdcl();int gettoken(void);int tokentype;      /* 最后一个标记的类型 */char    token[MAXTOKEN];        /* 最后标记字符串 */char    name[MXATOKEN];     /* 标识符名 */char    datatype[MAXTOKEN]; /* 数据类型为char,int等 */char    out[1000];          /* 输出字符串 */main()  /* 将声明转换为语句描述 */{    while (gettoken() != EOF) { /* 行中的第一个标记 */        strcpy(datatype, token);    /* 数据类型 */        out[0]  = ‘\0’;        dcl();              /* 解析行中其余部分 */        if (tokentype != ‘\n’)            printf(“syntax error\n”);        printf(“%s: %s %s\n”, name, out, datatype);    }    return 0;}

函数gettoken将会跳过空格和制表符,并找到输入中的下一个标记;token是一个名字,一对括号、大括号以及一个数字,或其他任何的单个字符。

int gettoken(void)  /* 返回下一个标记 */{    int c, getch(void);    void ungetch(int);    char *p = token;    while ((c = getch()) == ‘ ‘ || c == ‘\t’)        ;    if (c == ‘(‘) {        if ((c = getch()) == ‘)’) {            strcpy(token, “()”);            return tokentype = PARENS;        } else {            ungetch(c);            return tokentype = ‘(‘;        }    } else if (c == ‘[‘) {        for (*p++ = c; (*p++ = getch()) != ‘]’; )            ;        *p  = ‘\0’;        return tokentype = BRACKETS;    } else if (isalpha(c)) {        for (*p++ = c; isalnum(c = getch()); )            *p++    = c;        *p  = ‘\0’;        ungetch(c);        return tokentype = NAME;    } else {        return tokentype = c;    }}

getch和ungetch在第四章已被讨论。

进入某些内容比较简单,尤其是处理括号。程序undcl将描述声明的语句转换为声明,如将“x is a function returning a pointer to an array of pointers to functions returning char”转换为声明

char (*(*x())[])()

通过重用gettoken函数来缩小对输入处理的语法。undcl也使用dcl所使用的外部变量。

/* undcl:将语句描述转换为声明 */main(){    int type;    char temp;    while (gettoken() != EOF) {        strcpy(out, token);        while ((type = gettoken()) != ‘\n’)            if (type == PARENS || type == BRACKETS)                strcat(out, token);            else if (type == ‘*’) {                sprint(temp, “(*%s)”, out);                strcpy(out, temp);            } else if (type == NAME) {                sprint(temp, “%s %s”, token, out);                strcpy(out, temp);            } else                printf(“invalid input at %s\n”, token);        printf(“%s\n”, out);    }    return 0;}

练习 5-18。修改dcl程序处理错误输入。
练习 5-19。修改undcl,让undcl不给声明添加冗余的括号。
练习 5-20。扩展dcl程序处理函数参数、有const修饰符等的声明。

11.01

第六章:结构体

结构体是一个或多个变量的集合,这些变量可以是不同的类型,它们被组织起来并形参一个命名,这样就便于管理它们。(结构体在某些语言中被称为记录,诸如Pascal)。利用结构体可得到复杂的数据,尤其是在大型程序中,因为一组相关的数据可以被当成一个单元对待。

结构体的一个比较典型的例子是工资记录:一个雇员由诸如姓名、地址、社交安全号码、薪资等属性描述。部分属性也可以有结构体组成:名字由多个部分组成,地址甚至薪资也是一样。对C来说更加典型的一个来自图片的例子:一个点有一对坐标组成,矩形是一对点,等等。

ANSI标准对结构体的主要改变是定义了结构体的赋值 —— 结构体可以被复制和赋值,可以传递给函数,并可由函数返回。如今该属性已被精确定义,该特性也已被大多数编译器支持多年。自动的结构体和数组现在也可被初始化。

1 结构体的基础

11.02
让我们创造几个适用于图形的结构体。最基本的对象是一个点,假设由x轴和y轴的两个整数值组成。
这里写图片描述
这个点的两部分在结构体中可像如此声明:

struct  point {    int x;    int y;};

关键字struct引入结构体的声明,其内容在括号内。可选的结构体标签名可以紧跟在struct关键字之后(就是这里的point)。像这样的结构体标签,可以用作后续代替括号内容声明的简洁方式。

结构体内的变量名叫成员,结构体成员和标签之间以及和非结构体成员变量的命名可以相同,因为它们可由上下文被区分。另外,同名的变量可以出现在不同的结构体中,尽管作为良好风格来讲每个命名应该可以表示其含义。

结构体声明定义了一个类型。右大括号终止了结构体成员列表,在右大括号后面可以跟随该结构体类型的变量列表。也就是说,

struct  { … } x, y, z;

类似

int x, y, z;

即将x,y,z声明为相应类型的变量并为其分配相应的内存空间。

不会为不跟随变量列表的结构体声明分配内存空间;它仅是描述了结构体的模板或形状而已。若结构体包含了结构体标签,该标签就可以用来定义该结构体变量。例如,之前的point结构体,

struct  point   pt;

定义了变量pt,它的类型是struct point。结构体在定义时可被初始化,每个变量对应一个表达式,例如:

struct  point   maxpt = { 320, 200};

一个局部的结构体变量也能够通过赋值或通过调函数的返回值来初始化。

结构体成员的引用格式为:

structure-name.member

结构体成员操作符“.”用来联系结构体名和成员名。例如,欲打印点pt的坐标:

printf(“%d, %d”, pt.x, pt.y);

或计算pt到点(0, 0)的距离:

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坐标。

2 结构体和函数

11.03
对结构体合法的操作是复制它或将结构体当作一个单元进行赋值,通过使用操作符 & 可以得到结构体的地址并通过该地址来访问其成员。复制和赋值也包括将其传递给函数作为参数以及将其作为函数返回值。结构体不可以进行比较。结构体可以由常量值列表初始化;结构体的自动变量也可通过赋值进行初始化。
让我们编写操作点和矩形的函数来研究结构体。至少有三种方法:独立传递成员、传递整个结构体或传递结构体指针。每个方法都有其优缺点。

第一个函数makepoint带两个整型参数并返回一个point结构体:

/*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.pt2  = 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在矩形中返回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);    tmep.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并不是一个指针,所以这样的引用是不合法的。

结构体指针常被用到,因为它也有一种访问成员的方便的形式。如果p是一个指向结构体的指针,那么

p->member-of-structure

就引用了结构体相关的成员。(运算符 -> 由一个减号和一个大于符号组成)所以可以如下使用结构体指针访问结构体成员

printf(“orgin is (%d %d)\n”, pp->x, pp->y);

运算符 -> 的结合性由左至右,所以,若有以下定义

struct rect r, *rp = r;

那么以下四个表达式都是等价的

r.pt1.xrp->pt1.x(r.pt1).x(rp->pt1).x

结构体运算符 . 和 ->,以及函数 () 和 下标 [] 的优先级属于最高层次的。例如,给定以下声明

struct  {    int len;    char *str;} *p;

那么

++p->len

是len自增,而不是p自增,因为该表达式相当于有一个隐形的括号 ++(p->len)。括号可以用来跟某个运算符绑定在一起:(++p)->len在访问len前将p增1,(p++)->len将在访问len后p增1。

同理,*p->str将会访问str所指向的内容;*p->str++将在访问str所指内容后将str自增;(*p->str)++将str所指内容自增;*p++->str在访问str所指内容后将p自增。

3 结构体数组

11.04
编写一个用来计每个C关键字出现次数的程序。我们需要用一个字符串数组来保存关键字名,并用一个整型数组来对各关键字进行计数。可以同时使用两个数组keyword和keycount,如

char    *keyword[NKEYS];int keycount[NKEYS];

事实上,可以使用结构体数组来代替以上两个数组,两个关键字成为一个整体:

char    *word;int count;

结构体声明

struct  key {    char *word;    int count;} keytab[NKEYS];

定义了结构体变量keytab,并为keytab分配内存空间。数组中的每个元素都是一个结构体变量。该定义也可被写为

struct  key {    char    *word;    int count;};struct  key keytab[NKEYS];

因为结构体keytab包含的是一组常量值,所以可以将其定义为全局变量并在其定义时一次性初始化。结构体的初始化跟之前雷同 —— 在定义后用括号包含初始化部分:

struct  key {    char    *word;    int count;} keytab[] = {    “auto”, 0,    “break”, 0,    “case”, 0,    “char”, 0,    “const”, 0,    “continue”, 0,    “default”, 0,    /* … */unsigned”, 0,    “void”, 0,    “volatile”, 0,    “while”, 0};

初始化部分以对的形式出现以关联结构体成员。用括号将各个值括起来会显得更加精确,如

{“auto”, 0},{“break”, 0},{“case”, 0},…

当初始化部分比较简单时,内部的括号就不再有必要。通常,如果数组keytab的下标为 []时,编译器会根据初始化部分的初始化条目数计算出keytab有多大。

关键字计数程序开始于keytab的定义处。main函数调用一个名为getword的函数每次从输入行中读取一个单词。将读取到的单词用第三章提到的二分查找法在keytab中查找。如此,keytab中的关键字要以增序排列。

#include <stdio.h>#include <ctype.h>#include <string.h>#define MAXWORD 100int getword(char *, int);int binsearch(char *, struct key *, int);/* 计数C关键字 */main(){    int n;    char word[MAXWORD];    while (getword(word, MAXWORD) != EOF)        if (isalpha(word[0])            if ((n = binsearch(word, keytab, NKEYS)) >= 0)                keytab[n].count++;    for (n = 0; n < NKEYS; n++)        if (keytab[n].count > 0)            printf(“%4d %s\n”,                keytab[n].count, keytab[n].word);    return 0;}/* binsearch: 在在tab[0] … tab[n - 1]中查找word */int binsearch(char *word, struct key tab[], int n){    int cond;    int low, high, mid;    low = 0;    high    = n - 1;    while (low <= high) {        mid = (low + high) / 2;        if ((cond = strcmp(word, tab[mid].word)) < 0)            high    = mid - 1;        else if (cond > 0)            low = mid + 1;        else            return mid;    }    return -1;}

稍后还会展示getword函数;目前需要说明每次调用getword时会找到一个单词,它将该单词复制到其第一个参数中。

常量NKEYS是keytab中关键字的个数。尽管可以手动计算该值,但让机器计算会更加简单和可靠,尤其是当关键字对象发生改变的时候。在初始化部分可以用空指针来表示初始化列表的结束,那么循环就可以找到keytab的末尾了。

数组的长度完全可以在编译阶段求得。数组的长度是整个数组所占空间除以一个数组元素所占空间的值,所以数组长度为

size of keytab / size of struct key

C提供了编译阶段用来计算对象大小的一元运算符sizeof。表达式

sizeof object

以及

sizeof(类型名)

将得到一个等于指定对象或类型所占内存字节数的整数值。(严格来说,sizeof将会产生一个无符号整型,其类型为size_t,size_t被定义在头文件stddef.h中)这里的对象(object)可以是变量、数组以及结构体。类型名可以是类似int或double这样的基本类型名,也可以是结构体或指针这样的衍生类型。

在本例中,关键字的长度是数组除以单个元素的长度。该计算可以用#define语句来计算:

#define NKEYS   (sizeof keytab / sizeof(struct key))

另一个方法是用数组的长度来除以某个指定元素的长度

#define NKEYS   (sizeof keytab / sizeof keytab[0])

该方法的好处是,如果类型发生变化,该语句不需要作相应的改变。

sizeof不能再#if行中使用,因为预处理器不能解析类型名。但#define中的表达式不由预处理器求值,所以其中包含sizeof的代码是合法的。

现在来看getword函数。我们编写了一个不仅适用于此处的更为通用的getword函数,但它并不复杂。getword从输入流中获取下一个单词,该单词以字母或数字字符开头,或单个非空白字符。该函数值的返回值为单词的第一个字符,若输入结束则返回EOF,或返回的非字符的值。

/* getword:从输入流获取下一个单词或字符 */int getword(char *word, int lim){    int c, getch(void);    void ungetch(int);    char *w = word;    while (isspace (c = getch()))        ;    if (c != EOF)        *w++    = c;    if (!isalpha(c)) {        *w  = ‘\0’;        return c;    }    for ( ; --lim > 0; w++)        if (!isalnum (*w = getch())) {            ungetch(*w);            break;        }    *w  = ‘\0’;    return word[0];}

getword函数使用了第四章中编写的getch和ungetch函数。当字母数字的收集停止时,getword已经得到了超过一个字符的单词。调用ungetch将字符压回输入流供getword下一次读。getword使用isspace来跳过空白符,用isalpha来辨认字母,并用isalnum来辨认字母和数字;这些函数都在ctype.h中声明。

练习 6-1。本节中的getword不能适当地处理下划线、字符串常量、注释或预处理控制行。编写一个更好的版本。

4 结构体指针

11.06
欲说明涉及指针指向内容和结构体数组的一些注意事项,让我们再次编写计数关键字的程序,这次使用指针来代替数组索引。

外部声明keytab不需要改变,但是main和binsearch需要修改。

#include <stdio.h>#include <ctype.h>#include <string.h>#define MAXWORD 100int getword(char *, int);struct key *binsearch(char *, struct key *, int);/* 计数C关键字:指针版本 */main(){    char word[MAXWORD];    struct  key *p;    while (getword(word, MAXWORD) != EOF)        if (isalpha(word[0])            if ((p = binsearch(word, keytab, NKEYS)) != NULL)            p->count++;    for (p = keytab; p < keytab + NKEYS; p++)        if (p->count > 0)            printf(“%4d %s\n”, p->count, p->count);    return 0;}/* binsearch:从tab[0]…tab[n-1]中查找word */struct  key *binsearch(char *word, struct key *tab, int n){    int cond;    struct  key *low    = &tab[0];    struct  key *high   = &tab[n];    struct  key *mid;    while (low < high) {        mid = low + (high - low) / 2;        if ((cond = strcmp(word, mid->word)) < 0)            high    = mid;        else if (cond > 0)            low = mid + 1;        else            return mid;    }    return NULL;}

这里有几处值得注意的地方。首先,binsearch的声明必须表明它返回的是struct key类型指针而不是一个整数;在该函数原型和其内都应如此。如果binsearc找到了word,它将返回指向该word的指针,否则返回NULL。

其次,keytab的元素现由指针访问。这就导致binsearch函数的修改。
low和high的初始值分别为关键字表的开始和结束处。

中间元素的计算也不再是简单的

mid = (low + high) / 2; /* 错误 */

因为两个指针元素的加法是不合法的。然而减法是合法的,所以high - low就是元素个数,所以

mid = low + (high - low) / 2;

就将mid指向了low和high所指元素的中间元素。

最重要的修改是要调整算法来确保不要产生不合法的指针或对数组访问越界。&tab[-1]和&tab[n]都在数组tab之外。前者是绝对不合法的,间接引用后者也是不合法的。然而,语言定义并不保证指针于运算是否越界(也就是说&tab[n]会被认为合法)。

在main函数中,

for (p = keytab; p < keytab + NKEYS; p++)

p指向一个结构体,对p的元算会结合结构体的大小,所以p++会让p指向下一个结构体元素,在适当的时候该自增操作就会结束。

然而,不要假设结构体的大小是其内部成员大小之和。因为对于不同的对象来说会有对其需求,在结构体内有可能有“洞”。例如,如果char占一个字节且int占用4个字节,那么结构体

struct  {    char c;    int i;};

可能需要8个字节而不是5个字节。sizeof运算符将会返回其正确的字节数。

最后,程序格式:当函数返回诸如结构体指针这样的复杂类型时,如

struct  key *binsearch(char *word, struct key *tab, int n)

函数名就比较难被轻易看出来。一种可选的风格是

struct  key *binsearch(char *word, struct key *tab, int n)

这种风格书写属于个人爱好;您可选择你您自己偏好的格式编写程序。

5 结构体自我引用

11.07
假设我们想要处理在有的输入中计数所有单词的出现次数这样更通用的程序。由于不能提前得知会出现哪些单词,所以就不能提前对其排序并用二分查找法查找特定的单词。然而我们也不能用线性查找来查找某单词来看其是否已经出现过某单词;程序将会变得更长。(更精确地说,程序的运行时间可能会增至输入单词的二次方)如何组织数据来处理任意的单词?

一种解决方法是始终保持单词集,将每次读到的单词保存到其合适的位置上。单词的转移不会是线性的,这样也会让程序很长。如此,我们可以使用一个叫做二叉树的数据结构体来完成。

每个不同的单词都包含一个结点,每个结点包含:

指向文本中下一个单词的指针该单词出现的次数指向左子结点的指针指向右子结点的指针

每个阶段不会超过两个孩子;它可以没有或只有一个孩子。

以字母顺序组织该二叉树 —— 左子树上的单词都小于父结点上的单词,右子树上的单词都更大。如“now is the time for all good men to come to the aid of their party”的树结构为:
这里写图片描述
欲查找一个新单词是否在该树中,从根节点开始查其并将其跟根节点的单词比较。如果匹配,则将该节点上的计数增1。如果该单词小于该结点单词,则在左子树中查找该单词;如果该单词大于该结点单词,则在右子树查找该单词。如果最终没有查找到该单词,则在树的末尾追加该单词。该过程可以是递归的。

回到描述结点的地方,它可以由一个结构体描述:

struct  tnode { /* 树结点: */    char    *word;  /* 指向单词 */    int count;  /* 单词出现的次数 */    struct  tnode *left;    /* 左孩子 */    struct  tnode *right;   /* 右孩子 */};

以上递归声明看起来有些难解,但它的确是正确的。对于结构体来说,不可以包含其自身的实例,但

struct  tnode *left;

声明了left为指向tnode的指针变量,而不是一个tnode变量。

有时候,需要相关引用某个结构体:两个结构体互相引用。实现的方法为:

struct  t {    …    struct s *p;    /* p指向struct s类型 */};struct  s {    …    struct t *q;    /* q指向struct t类型 */};

该程序代码量出奇地小,就像已经编写的getword那样小。main程序用getword来读单词并用addtree来将读到的单词添加到二叉树中。

#include <stdio.h>#include <ctype.h>#include <string.h>#define MAXWORD 100struct tnode    *addtree(struct tnode *, char *);void    treeprint(struct tnode *);int getword(char *, int);/* 单词出现频率统计 */main(){    struct tnode *root;    char word[MAXWORD];    root    = NULL;    while (getword(word, MAXWORD) != EOF)        if (isalpha(word[0]))            root    = addtree(root, word);    treeprint(root);    return 0;}

函数addtree是递归的。单词由main函数将其放置到树的根节点。在每一步中,单词都会跟已经存储在树中的单词比较,并会被addtree放置到相应位置。如果该单词已经在树中,则该单词的出现次数增1,或者被添加到树的末尾。如果新结点被创建,addtree将返回指向该结点的指针。

struct  tnode   *talloc(void);char    *strdup(char *);/* addtree:增加含w的结点到p或在p下面 */struct tnode *addtree(struct tnode *p, char *w){    int cond;    if (p == NULL) {    /* 新单词 */        p   = talloc(); /*创建一个新结点 */        p->word = strdup(w);        p->count    = 1;        p->left = p->right = NULL;    } else if ((cond = strcmp(w, p->word)) == 0)        p->count++; /* 重复的单词 */    else if (cond < 0)  /* 小于,则进入左子树 */        p->left = addtree(p->left, w);    else        p->right    = addtree(p->right, w);    return p;}

11.08
新结点的存储空间由talloc程序分配,该函数返回一个指向能容纳树结点内存空间的指针,新出现的单词会被strdup函数拷贝到该内存空间相应位置处。(待会再讨论该程序)计数被初始化,两个孩子都设置为NULL。这部分代码在有新结点加入时被执行。我们忽略了检查strdup和talloc返回值错误的情况。

treeprint按序打印树中各结点;在每个结点处,它首先打印其左子树(所有的单词都比该结点中的单词小),然后再打印该结点中的单词,然后再打印右子树(更大的单词)。如果你对递归的工作机制感到迷惑,可脑动打印之前一例中的结点来模拟treeprint的打印。

/* treeprint:按序打印树p中的结点 */void    treeprint(struct tnode *p){    if (p != NULL) {        treeprint(p->left);        printf(“%4d %s\n”, p->count, p->word);        treeprint(p->right);    }}

在实际中要注意:如果因为单词不是随机而来使得树变得“不平衡”,程序的运行时间将会增加不少。在最坏的情况下,即单词已有序的方式输入,那么程序跟线性查找时间复杂度一样。通常由几种方法来产生二叉树而不必遭受最坏情形,但我们不在这里描述该方法。

之前留下的这个例子,也值得在关于存储分配上作简要讨论。即程序中只有一个程序分配器,即使为不同的对象分配空间。但是如果分配器按照需求分配,也就是说,指针是指向char还是struct tnodes。首先,如何满足具体机器上的对其要求(如,整型需要位于偶数地址上)?其次,需要如何声明分配器的返回的指针类型?

对其要求可被轻易解决,可以使用一个最耗空间类型来进行对其。第五章的alloc没有保证特定的对其要求,所以我们将使用库函数malloc来分配空间。在第八章会讨论malloc的实现。

如何声明像malloc这样的函数是任何要进行严格类型检查的语言的一个难题。在C中,一个适用的方法是将malloc声明为指向void类型的指针,然后再精确的将该指针转换为相应类型。malloc和相关的代码在stdlib.h中声明。因此,talloc可以被写为

#include <stdlib.h>/* talloc:创建tnode */struct tnode *talloc(void){    return (struct tnode *)malloc(sizeof(struct tnode));}

strdup仅是将参数给定的字符串拷贝到一个安全的内存空间中,该空间由调用malloc获得。

char *strdup(char *s) /* make a duplicate of s */{    char *p;    p   = (char *) malloc(strlen(s) +1);    /* +1 for ‘\0’ */    if (p != NULL)        strcpy(p, s);    return p;}

如果没有可用的空间,malloc将会返回NULL;strdup并没有检查对此作错误处理。

由malloc分配的空间再由free函数回收后,该空间可以被重新使用;见第7章和第8章。

练习 6-2。编写一个读取C程序的程序,以字母顺序打印与某变量前6个字符相同但后续字符不同的变量。不要将字符串和注释计算在内。让该变量参数能以命令行参数的形式传递给程序。
练习 6-3。编写一个交叉引用(cross-referencer)打印文档中的所有单词,并且要输出每个单词所出现的行号。可以不打印诸如the、and这样的单词。
练习 6-4。编写程序将不同的单词以其频率的递减顺序打印。

6 查表

11.09
本节我们将编写查表程序的一部分,来演示结构体的其他方面。这段典型的程序可能会被在宏预处理器或编译器中找到。例如,思考#define语句。如下行

#define IN  1

出现时,名字IN和其替换文本1被存储在一个表中。后续若出现在某语句中,如

state       = IN;

IN将会被替换为1。

有两段操作名字和替换文本的程序。install(s, t)将名字s和替换文本t记录到表中,然后返回所找到的IN所在位置的指针,如果没有找到就返回NULL。

其中的算法为哈希查找 —— 输入名将会被转换为一个小型非负整数,它被用来作为指针数组的索引。每个数组元素指向描述拥有哈希值得名字列表的开始处。如果哈希表中没有该名字的哈希值则为NULL。

列表中的块是包含指向名字的指针、替换文本以及列表中下一个块。空指针表示列表结束。

struct  nlist { /* 表入口: */    struct  nlist   *next;  /* 链中的下一个入口 */    char    *name;          /* 定义的名字 */    char *defn;         /* 替换文本 */};

指针数组为

#define HASHSIZE        101static  struct  nlist *hashtab[HASHSIZE];   /* 指针表 */

哈希函数,会被lookup和install调用,它将每个字符值添加到字符串中以和之前的一个字符串联合并返回模块的长度。这不是最好的哈希函数,但是它的代码短且有效。

/* hash:为字符串s生成哈希值 */unsigned hash(char *s){    unsigned    hashval;    for (hashval = 0; *s != ‘\0’; s++)        hashval = *s + 31 * hashval;    return hashval % HASHSIZE;}

无符号的运算保证哈希值为非负数。

哈希过程将产生hashtab的开始索引值;如果字符串在其他地方被找到,他讲在列表模块的开始处。查找由lookup完成。如果lookup已经找到了入口,就将返回一个指向他得指针。如果没有找到,则返回NULL。

/* lookup:在hashtab中查找 s */struct nlist *lookup(char *s){    struct nlist *np;    for (np = hashtab[hash(s)]; np != NULL; np = np->next)        if (strcmp(s, np->name) == 0)return np; /* 找到 */    return NULL;    /* 没有找到 */}

在lookup中的for循环是查找整个链表的标准方法:

for (ptr = head; ptr != NULL; ptr = ptr->next)    …

install使用lookup来判断一个名字是否已经存在表中了;如果存在,新的定义将会取代就得一个。否则,新入口将会被创造。如果没有空间时,install会返回NULL。

struct  nlist *lookup(char *);char *strdup(char *);/* install:在hashtab中put(name, defn) */struct nlist *install(char *name, char *defn){    struct  nlist *np;    unsigned hashval;    if ((np = lookup(name)) == NULL) {  /* 没有找到 */        np  = (struct nlist *) malloc(sizeof(*np));        if (np == NULL || (np->name = strdup(name)) == NULL)            return NULL;        hashval = hash(name);        np->next    = hashtab[hashval];        hashtab[hashval]    = np;    } else  /* 已经存在 */        free((void *)np->defn); /* 释放之前的defn */    if ((np->defn = strdup(defn)) == NULL)        return NULL;    return np;}

练习 6-5。编写undef函数,该函数移除lookup和install维持的在表中的名字和定义。
练习 6-6。基于本节的程序实现简单版本的适合C程序使用的#define处理器(如,无参数)。可能之前的getch和ungetch会有所帮助。

7 typedef

11.14
C语言提供叫作typedef的工具来定义新数据类型名。例如声明

typedef int Length;

将使得Leng跟int同效。之后,类型Length可以用在声明、转换等中,它的效果跟使用int一样:

Length  len, maxlen;Length  *lengths[];

同理,声明

typedef char *String;

将使得String跟char*同效,之后,它可以使用在声明或转换中:

String  p, lineptr[MAXLINES], alloc(int);int strcmp(String, String);p   = (String)malloc(100);

注意在typedef中的类型声明出现在变量的位置上,而不是紧跟typedef之后。从语法上讲,typedef跟存储声明extern、static等类似。我们通常将typedef声明的名字的首字母大写,以让它们突出一些。

对于更加复杂的例子,我们可以用typedef来定义之前章节提到的树结点:

tpedef  struct  tnode *Treeptr;typedef struct  tnode { /* 树结点:*/    char    *word;          /* 指向下一个单词 */    int count;          /* 出现次数 */    Treeptr left;           /* 左孩子 */    Treeptr right;      /* 右孩子 */} Treeptr;

这段代码创建了两个新类型关键词,Treenode(结构体)和Treeptr(结构体指针)。那么,程序talloc将变为

Treeptr talloc(void){    return (Treeptr) malloc(sizeof(Treenode));}

必须强调的是,typedef声明并没有创造一个新的数据类型;它仅仅是为存在的数据类型增加了一个名字。该名字没有任何新的语义:它声明的变量跟用原来的类型声明一样。实际上,typedef有点像#define,但typedef是由编译器翻译的,编译器能处理的超过预处理能处理的进行文本替换功能之外。例如

typedef int (*PFI) (char *, char *);

将会创造类型PFI,它是一个指向返回整型参数为两个字符指针的函数的指针,它可以被如下使用

PFI strcmp, numcmp;

这两个函数在第五章被提到过。

除了美观,还有两个使用typedef的原因。第一个程序的可移植问题。如果typedef用于基于机器的数据类型,程序移植时只需要更改typedef部分即可。一个常见的情况是使用typedef名来定义各种类型的整数,为short,int,long分配一个合适的名字。像标准库中的size_t和ptrdiff_t就是一个典型的例子。

使用typedef的第二个目的是为程序提供一个更好的文档说明 —— 名为Treeptr比一个复杂的结构体指针更易被理解。

8 联合

11.15
通过编译器追踪大小和对其需求,联合是能够(在不同时间)保持不同类型和大小对象的变量。联合提供了在一块内存中操作不同类型数据的方式,不用在程序中插入任何基于机器信息的代码。它跟Pascal中的变体记录类似。

举一个可以在编译器符号表管理器中找到的例子,假设可能为int、float或者字符指针的常量。特殊常量的值必须存储在一个合适的变量中,对于表管理器来说最方便的方法是将这些不同类型的变量都存储在一块空间中。这就是联合的目的 —— 可以存储任何类型数据的单个变量。它的语法基于结构体:

union   u_tag {    int ival;    float    fval;    char *sval;} u;

变量u要足够大以能够存储三个变量中尺寸最大的变量;它们存储大小依具体实现。这三个变量都存储在u中,所以用法是一致的:最近存储的类型必须要被检索。跟踪当前存储在联合中的类型由程序员负责;如果存储类型跟提取联合值的类型不一致的结果基于具体实现。

访问联合的成员的语法为

union-name.member

或者

union-pointer->member

就跟结构体一样。如果变量utype用来跟踪当前存入u中的类型,那么跟踪代码有可能如下:

if (utype == INT)    printf(“%d\n”, u.ival);else if (utype == FLOAT)    printf(“%f\n”, u.fval);else if (utype == STRING)    printf(“%s\n”, u.sval);else    printf(“bad type %d in utype\n”, utype);

联合也可以出现在结构体和数组中,反之亦然。在结构体中访问联合的成员的标记跟结构体嵌套时相同(反之亦然)。例如,结构体数组的定义为

struct {    char *name;    int flags;    int utype;    union {        int ival;        float fval;        char *sval;    } u;} symtab[NSYN];

访问成员ival的方式为

symtab[i].u.ival

以及字符串sval的第一个字符的访问方式可以为

*symtab[i].u.svalsymtab[i].y.sval[0]

实际上,联合是所有成员偏移基址为零的结构体,该结构体足够大以能够存储最大的成员,以及联合中类型的合适的对齐。结构体的相关的操作都能用于联合之上:赋值或将其当成一个单元复制,取其地址以及访问成员。

联合可能只能被其第一个成员的类型的数据来进行初始化,因此以上描述的联合只能被一个整型值初始化。

第8章的存储分配器将展示使用联合来强制对其特定类型存储空间的边界。

9 位域

11.16
当存储空间很稀缺时,可能就需要将多个对象放到单个机器字里;一种常见的使用是像编译应用程序中符号表单位的一套单位标志。外部的数据格式,如硬件设备接口,也常需要从一个字里面获得几个位。

假设编译器中操作符号表的一段程序。程序中的每个标识符都有一个和其关联的确切的信息,不管它是否是一个关键字,它是否是外部或静态的等。最常见的方式是将一个位标志设在一个char或int中。

常见的方法是定义一套“屏蔽”来透漏相应位的位置,如

#define KEYWORD 01#define EXTERNAL    02#define STATIC      04

enum    { KEYWORD = 01, EXTERNAL = 02, STATIC = 04 };

这些数字必须是2的指数。访问位就变成了第二章所描述的移位、屏蔽以及疑惑的位操作。这些代码类似

flags   |= EXTERANL | STATIC;

打开flags的EXTERNAL和STATIC位。

代码

flags   &= ~(EXTERNAL | STATIC);

就屏蔽了flags的EXTERNAL和STATIC位。

代码

if ((flags & (EXTERNAL | STATIC)) == 0) …

将测试这两位是否被屏蔽。

尽管这些写法的代码已经比较成熟了,但C提供了另外一种可用来实现类似功能的语法。即位域,它是被定义的单个空间被称为字中的邻近的几个位。位域的定义和使用的语法都是基于结构体的。例如,以上的#define符号表可以被以下位域代替:

struct  {    unsigned int    is_keyword : 1;    unsigned int is_extern : 1;    unsigned int is_static : 1;} flags;

该定义了一个叫做flags的变量,它包含了3个1位的域。冒号后面的数字位域的宽度。位域被声明为unsigned int是为了确保位域中的数为正数。

每个位域成员的引用跟结构体成员的引用一样:flags.is_keyword,flags.is_extern等。位域的行为就像小型的整数,它们可以像其他整数一样出现在算数表达式中。因此之前的例子可以被写为:

flags.is_extern     = flags.is_static = 1;

就将这些位打开了;

flags.is_extern = flags.is_static = 0;

就将这些位屏蔽掉了。

if ((flags.is_extern == 0 && flags.is_static == 0)    …

就是在测试这些位是否被屏蔽了。

几乎所有跟位域相关的东西都是基于具体的实现。位域是否能重叠到字的边界基于具体实现。域可能不需要被命名;没有命名的域(只用一个冒号和长度值)用作填充使用。宽度0是一个特殊的值,它的作用是强制和下一个字边界对其。

对域赋值时,在有些机器上的顺序是从左到右,而在另外一些机器上却是从右到左。这意味着尽管位域对维持内部定义的数据结构比较有用,但它的问题是谁首先截止需要被仔细考虑当定义外部数据时;基于这样特性的程序的移植性是很差的。域可能只能被声明为整型;为了移植性,需要确切的指定signed或unsigned int。它们不能是数组,也不能对它们进行取址,所以 & 操作符不能用于位域。

第七章:输入和输出

12.04
输入和输出部分本身并不是C语言的一部分,所以在此之前该部分内容都没有被呈现。然而,程序和环境的交互远比之前所展示的方式要复杂得多。在本章中将描述一个标准库,该库提供了一套输入和输出、字符串处理、存储管理、算数运算以及为C提供其他多样服务的函数。

ANSI标准准确的定义了这些库,所以在支持C的系统上都存在这些库。使用标准库提供的函数编写的程序移植到另外一个平台时不用修改源代码。

标准库函数包含在多个头文件中;之前已经见到了几个,包括stdio.h,string.h以及ctype.h。我们不在这里呈现完所有的头文件,因为我们对在编写C程序时怎么使用它们更感兴趣。库在附录B中被详细描述。

1 标准输入输出

正如第一章所述,库实现了文本输入和输出的简单模型。文本流由连续的行组成;每行由换行字符结束。如果系统不已该机制运行,库就有必要让文本以这种方式出现。例如,库需要将回车转换为换行并回到新行的开始处。

最简单的输入机制是每次从标准输入(通常是键盘)读取一个字符,通常可以用C库中的getchar函数:

int getchar(void)

getchar函数会返回输入流中的下一个新的字符,若遇到文件结束则返回EOF。符号常量EOF定义在stdio.h文件中。EOF的值通常是-1,但在程序中应该使用EOF,以确保它是独立于文件内容的一个值。

在许多环境中,可以用 < 转换符来实现重定向:如果程序prog使用getchar,命令行

prog <infile

就会让prog从infile这个文件中读取字符。通过以上方式让输入发生了变化,在实际编程中,字符串“< infile”不会被包含到命令行参数argv中。通过管道机制也可以让输入发生变化:在某些系统上,命令行

otherprog | prog

将会运行otherprog和prog两个程序,并且管道会让otherprog的标准输出定向给prog的标准输入。

函数

int putchar(int)

用作输出:putchar(c)将字符串c打印在标准输出上,标准输出一般为屏幕。putchar会返回所输出的字符,如果发生错误则返回EOF。同理,也可以使用

prog >outfile

将会将原本输出到标准输出的内容输出到outfile文件中。如果系统支持管道,那么

prog | anotherprog

将会把prog原本输出到标准输出的内容作为anotherprog的标准输入。

由printf产生的输出也会通过它的方式将内容输出到标准输出。调用putchar和printf可能会是交叉的 —— 按照被调用的顺序各自的内容相序的出现在屏幕上。

每个涉及输入/输出的源文件在引用输入输出函数前都必须包含行

#include <stdio.h>

当头文件由

< >

括起来时,它搜寻头文件的方式为:到头文件所在的路径下寻找(例如,在UNIX系统上,典型的头文件目录为/usr/include)。

许多程序都只读一个输入流并只写一个输出流;对于这样的程序,使用getchar、putchar以及printf这样的函数就已经足够了。如果重定向来连接程序的输入输出也是可以的。例如,考虑程序lower,他将输入的字符转换为小写字符:

#include <stdio.h>#include <stype.h>main()  /* lower: 将输入转换为小写字符 */{    int c;    while ((c = getchar()) != EOF)        putchar(tolower(c));    return 0;}

函数tolower定义在头文件ctype.h中;它将大写字母转换为小写字母,它还会返回非字母的字符。正如之前所提,在头文件stdio.h中的getchar、putchar和头文件ctype.h中的tolower这些函数通常都是宏。我们将会在8.5节展示他们是如何实现的。尽管ctype.h中的函数的实现基于具体的机器,但它们的存在会屏蔽具体机器的不同之处。

练习 7-1。编写将大写字母转换为小写字母或将小写字母转换为大写字母的程序,基可执行文件的名字,即argv[0]。

2 格式输出 —— printf

输出函数printf将内部值转换为字符。我们之前的章节已经使用过了printf函数。本节的描述将会涉及该函数大多数典型的使用,但并不完整;完整见附录B。

 int printf(char *format, arg1, arg2, ...)

printf函数转换、格式化并将参数输出到标准输出中。它返回所打印的参数个数。

格式化字符串包含两种类型的对象:普通字符,这些普通字符会被输出到输出流中;还有转换符,每个转换符将会转换并打印printf中相应的参数。每个转换符以一个 % 字符开始,并以特定的转换字符结束。在 % 和特定的转换字符之间按照顺序可以是:

  • 减号,用来指定转换参数向左对齐。
  • 用来指定最小宽度域的数字。转换参数将以数字作为打印的最小宽度。如果有必要将会在左边(或右边)填充空格来达到宽度域。
  • 一个点,用来隔开宽度域和精度。
  • 用来指定精度的数字,该值用来指定字符串打印的最大字符数,十进制浮点数的小数位数,或者整数的最小个数。
  • h,对于整数来说,再跟h则表示以short的方式打印;或者是l(字符ell),那么就会以long类型打印整数。

    转换字符如表7-1所示。如果 % 后的转换字符不属于表中所列举,则具体行为无定义。

    转换字符参数类型;以该类型打印 d, iint;十进制数 ounsigned int;无符号八进制数(无开头的0) x, Xunsigned int;无符号十六进制数(无开头的0x或0X,使用abcdef或ABCDEF代表10,…,15 uunsigned int;无符号十进制数 cint;单个字符 schar *;打印以’\0’结束的字符串或指定精度字符的个数 fdouble;[-]m.dddddd,d个数由精度指定(默认为6) e,Edouble;[-]m.dddddde+-xx或者[-]m.ddddddE+-xx,d的个数由精度指定(默认为6) g,Gdouble;当指数小于-4或大于或等于精度时为%e或%E;否则为%f。末尾的0和小数点不会被打印 pvoid *;指针(基于具体类型的实现) %无转换参数;打印%

    宽度和精度可以被指定为 *,此种情况下该值有下一个参数指定(必须为int)。例如,最多打印字符串的max个字符,

 printf("%.*s", max, s);

大多数的格式转换在之前的章节中已演示。一个没有被演示的例外是跟字符串相关的精度。下表展示了打印“hello world”(12个字符)精度的几种方法。用冒号来隔开每个域,这样就可以更清晰看到结果。

:%s:        :hello, world::%10s:      :hello, world::%.10s:     :hello, wor::%-10s:     :hello, world::%.15s:     :hello, world::%-.15s:    :hello, world   ::%15.10s:   :     hello, wor::%-15.10s:  :hello, wor     :

注意:printf使用第一个参数来判断有多少个参数以及每个参数的类型。如果没有足够的参数或参数的类型错误,将会得到错误的打印结果。您应该要意识到以下两种调用的区别:

printf(s);/*FAILS 如果s包含 %*/printf("%s", s);/*安全*/

函数sprintf做跟printf一样的转换,但是它的输出存储在一个字符串中:

 int sprintf(char *string, char *format, arg1, arg2, ...)

sprintf根据format中的格式格式化参数arg1, arg2等,将结果保存到字符串string中;所以string必须要足够大以容下该结果。

练习7-2。编写一个程序以灵活的方式打印任意的输入。最起码的功能应该有,根据本地编码以八进制或十六进制打印非图形字符,并折断比较长的文本行。

7.3 可变参数列表

本节包含实现最小版本的printf的内容,以展示怎么编写可移植的处理可变参数的函数。因为我们主要对处理参数的处理感兴趣,所以所编写的minprintf将会处理格式化字符串和参数并会调用printf来进行格式化转换。

原printf函数的声明为:

 int printf(char *fmt, ...)

声明中的…表示参数的数量和类型可以变化。声明…只能出现在参数列表的末尾。所以我们的minprintf的声明为

 void minprintf(char *fmt, ...)

该函数不会返回返回所打印的参数个数,所以返回值类型为void。

minprintf的微妙之处在于它如何遍历其参数列表,可变参数列表甚至连一个名字也没有。标准头文件stdarg.h包含了一套如何遍历参数列表的宏定义。该头文件的的实现会因机器的不同而不同,但是它提供的接口是一致的。

类型va_list用来声明依次摄取每个参数的变量;在minprintf中,该变量为ap,代表参数指针的意思。宏va_start初始化ap指向第一个未命名的参数。在使用ap之前必须调用其来将其初始化。参数列表中必须至少要有一个命名的参数;最后命名的参数会被va_start用作为开始。

每次调用va_arg都将会返回一个参数并让ap指向下一个参数;va_arg使用类型名来判断要返回的类型以及计算要移动多少距离。最后,va_end会完成清理工作。必须在函数结束前调用该函数。

这些属性可以构成一个简单的printf:

#include <stdarg.h>/* minprintf:最小版本的可变参数列表的printf */ void minprintf(char *fmt, ...) {     va_list ap; /* 依次指向每个未命名的参数 */     char *p, *sval;     int ival;     double dval;     va_start(ap, fmt); /* 让ap指向第一个未命名的参数 */     for (p = fmt; *p; p++) {         if (*p != '%') {             putchar(*p);             continue;        }        switch(*++p) {        case 'd':            ival = va_arg(ap, int);            printf("%d", ival);            break;        case 'f':            dval = va_arg(ap, double);            printf("%f", dval);            break;        case 's':            for (sval = va_arg(ap, char *); *sval; sval++)                putchar(*sval);            break;        default:            putchar(*p);            break;        }    }    va_end(ap); /* 当处理完成时清理 */}

练习 7-3。修正minprintf以处理printf的其他更多功能。

12.09

4 格式化输入 —— scanf

scanf跟printf类似,只不过它提供的跟输入相关的转换:

 int scanf(char *format, ...)

scanf从标准输入中读取字符,并根据format中的转换翻译输入,并将结果存入scanf后续的参数中。参数format后续再描述;剩余参数每个都必须是一个指针类型,这些地址就是每个输入所要存储的地方。跟pritnf一样,该节只是对scanf最常用的特征作一个总结,并不将scanf的每样功能都列举出来。

当format参数被遍历完或输入跟format中个转换不匹配时,scanf就停止从标准输入读取字符。它返回成功读取的个数并将各个条目保存到相应的参数中。其返回值可以用来判断该函成功读取了多少条目。若遇到文件结束,则返回EOF;注意返回的EOF跟0不一样,0表示输入的第一个条目跟scanf中format参数中的第一个转换不匹配。每调用scanf,scanf就从刚已经被转换后的下一个参数保存输入中的条目。

也有sscanf函数从字符串中读取字符,而不是从标准输入:

 int sscanf(char *string, char *format, arg1, arg2, ...)

该函数根据format从string中读取内容,并将结果依次存入arg1,arg2等所代表的变量中。这些变量必须为指针。

format参数通常包含转换字符,转换字符用来控制输入的转换。format字符串可以包含:

  • 将会被忽略的制表符或空格。
  • 普通字符(非%),它们用来匹配输入流中下一个非空白的字符。
  • 转换规格,由字符%和可选的用来说明忽略不读字符数的*,可选的用来指定最大宽度的数字,可选的用来表示目标宽度的h, l或L,以及一个转换字符。
    转换规格是对输入域的直接转换。通常结果被保存在相应参数代表的地址中。如果需要忽略的字符数由*指定,有的输入字符将会被跳过。输入被定义为非空白的字符串;它扩展到下一个非空白字符或知道所指定的宽度。这就意味着scanf会跨越行界而找到其输入,因为换行为空白。(空白字符为空格,制表符,换行符,回车符,垂直制表符以及换页符)

    转换字符表示对输入文本的翻译机制,相应的参数必须是一个指针,因为C的传参机制为传值。转换字符如表7-2所示。

字符输入数据;参数类型 d十进制整数;int * i整数;int *。整数可以是8进制或16进制 o八进制整数(有或没有开头的0);unsigned int * u无符号十进制数;unsigned int * x十六进制整数(有或无开头的0x或0X);unsigned int * c字符;char *。下一个输入的字符(默认1个)将会被保存在相应的地方。通常,空白符会被指定跳过;读取下一个非空白字符,使用%1s s字符串(无引号);char *,指向一个字符串数组足够大能够容下字符和结束符’\0’的空间 e, f, g拥可选符号的浮点数,可选的十进制和可选的指数;float * %字母%;不会有任何的分配执行

转换字符d, i, o, u以及x之前可能会由h来表明参数列表中的指针是short类型而非int,或者用l(字母ell)来表示参数列表中的指针参数为long。同理,转换字符e, f以及g之前可以放置l来表示参数列表中相应的指针是指向double而不是float。

将第四章的计算器作为第一个例子,来数码scanf的格式转换:

#include <stdio.h>main() /* 未成熟的计算器 */{    double sum, v;    sum = 0;    while (scanf("%1f", v) == 1)        printf("\t%.2f\n", sum += v);    return 0;}

假设我们读取的是以下包含日期的输入:

25 Dec 1988

sanf语句就要相应地为:

 int day, year; char monthname[20]; scanf("%d %s %d", &day, monthname, &year);

不用对monthname使用&地址符,因为数组名本身就代表一个指针。

字母也可以出现在scanf中的format参数中;它们必须跟输入的字母相匹配。所以我们可以以mm/dd/yy的格式来用scanf读取语句:

 int day, month, year; scanf("%d/%d/%d", &month, &day, &year);

scanf忽略其format中的空格和制表符。另外,他将会跳过空白字符(空格、制表符、换行符等)而继续查找输入值。欲读取格式不固定的输入行,最好的方式先一次性读取一行,然后用sscanf来将其分离。例如,假设我们想要读取包含以上提到的日期的行,那么可以像以下这样写代码:

 while (getline(line, sizeof(line)) > 0) {     if (sscanf(line, "%d %s %d", &day, monthname, &year) == 3)         printf("valid: %s\n", line) /* 25 Dec 1988格式 */     else if (sscanf(line, "%d/%d/%d", line; /* mm/dd//yy格式*/         printf("valid: %s\n, line);     else         printf("invalid: %s\n", line); }

调用scanf可能会和对其他函数的调用发生混淆。下一个调用的其他输入函数将会从scanf所读停止的地方开始。

最后的警告:scanf和sscanf的参数都必须是地址。目前最常见的错误就是:

 scanf("%d", n)

而没有写成:

scanf("%d", &n);

这个错误不会都会被编译器检测到。

练习 7-4。编写个人版本的minprintf来模拟scanf的功能。
练习 7-5。重新编写第四章的前缀计算器,使用scanf和/或sscanf来作输入和数字的转换。

5 文件访问

12.13
目前我们所有例子中所读的标准输入和写的标准输出都是由特定操作系统已经定义了的。

接下来编写跟程序无任何关联的访问文件的程序。能验证该功能的一个程序是cat,它将一系列文件的内容打印到标准输出中。cat将文件的内容打印到屏幕之上,作为收集输入目的程序没有能通过文件名而访问文件的功能。例如,命令

cat x.c y.c

会将文件x.c和y.c的内容输出到标准输出中。

如何让安排具有命名的文件被读取 —— 也就是如何将用户能见到的文件名和程序语句联系起来而获取该文件的内容。

该规则很简单。在读取或写之前,文件已经被库函数fopen打开。fopen以诸如x.c或y.c这样的外部文件名作为参数,通过联合操作系统做一些工作(具体不用关心),最后返回可以读或写该文件的指针。

该指针,被称为文件指针,它指向一个包含文件缓冲区、当前字符在缓冲区位置、文件被读还是被写、是否发生错误或已到文件末尾的结构体。用户不必知道这些细节,因为在头文件stdio.h中定义了该结构体,其名为FILE。声明文件指针的方式也很简单,形式如下

FILE *fp;FILE *fopen(char *name, char *mode);

fp是一个指向FILE的指针,fopen函数将返回一个指向FILE的指针。注意FILE是一个诸如int的类型名,并不是一个结构体标签;它有typedef定义而来。(细节见8.5节中在UNIX中实现fopen部分)

在程序中调用fopen的形式为

fp  = fopen(name, mode);

fopen的第一个参数是包含文件名的字符串。第二个参数为模式,也是一个字符串,它包含对文件操作的意图。可允许的模式包含读(”r”),写(”w”)以及附加(”a”)。一些系统区分文本和二进制文件;对于后者,”b”必须附加到模式字符串中。

在执行写或附加操作时,若文件不存在,则就先创建该文件。打开已存在的文件并对其进行写操作会擦除原文件内容,如果对文件进行附加操作,则原文件内容会被保留。对不存在的文件进行读操作会发生错误,诸如对无读取权限的文件进行读操作还会引起其他的错误。如果出错,fopen会返回NULL。(该错误可以被更准确的获取;见附录B中第1节末尾对函数错误处理的讨论)

在打开文件之后,下一步就是确定读或写文件的方式。可以用几种方法实现,其中使用getc和putc是最简单的方式。getc返回文件中的下一个字符;它需要指向具体文件的文件指针作为参数。

int getc(FILE *fp)

getc返回由fp关联的文件流中的下一个字符;当文件流结束或发生错误时返回EOF。

putc是一个输出函数:

int putc(int c, FILE *fp)

putc像fp关联的文件中写入字符c并返回缩写的字符,如果发生错误则返回EOF。类似getchar和putchar,getc和putc都是宏定义而非函数。

当C程序开始,操作系统环境负责打开三个文件并提供相应的文件指针。这几个文件分别是标准输入、标准输出以及标准错误;相应的文件指针并称为stdin,stdout以及stderr,它们被声明在stdio.h中。通常,stdin和键盘关联,stdout以及stderr和屏幕关联,但是stdin和stdout也可以被重定向到文件或管道(7.1节中所描述)中。

getchar和putchar可以被getc、putc、stdin以及stdout由下定义而来:

#define getchar()   getc(stdin)#define putchar()   putc((c), stdout)

对于具有一定格式的输入或输出文件,fscanf和fprintf函数可以发挥作用。它们和scanf和printf相同,除了第一个参数是关联具体被读写的文件指针外;格式字符串是第二个参数

int fscanf(FILE *fp, char *format, …)int fprintf(FILE *fp, char *format, …)

通过这些已有的方式,我们可以编写程序cat来关联文件。该设计对许多程序都可用。如果存在命令行参数,它们将会被依次当作文件名使用。如果没有参数,程序就处理标准输入。

#include <stdio.h>/* cat:关联文件,版本1 */main(int argc, char *argv[]){    FILE    *fp;    void filecopy(FILE *, FILE *);    if (argc == 1) /* 无参数:复制标准输入 */        filecopy(stdin, stdout);    else        while (--argc > 0)            if ((fp = fopen(*++argv, “r”)) == NULL) {                printf(“cat: can’t open %s\n”, *argv);                return 1;            } else {                filecopy(fp, stdout);                fclose(fp);            }    return 0;}/* filecopy: 复制文件ifp到文件ofp */void filecopy(FILE *ifp, FILE *ofp){    int c;    while ((c = getc(fp)) != EOF)        putc(c, ofp);}

文件指针stdin和stdout都是FILE *类型的实际对象。它们是常量,所以不可再对其赋值。
函数

int fclose(FILE *fp)

跟fopen函数相反;它将由fopen函数给文件指针和文件名建立的关系消除,将文件指针释放给其他文件。因为大多数操作系统对同时操作的文件数量有限制,对不再使用的文件指针进行释放是一个好的习惯。fclose同时还刷新putc函数相应的缓冲区,以让该缓冲区的内容立即输出到相应的文件中。当程序不再使用某个文件指针时,要自动使用fclose来关闭该文件指针。(如果不再需要stdin和stdout也可以关闭它们。标准输入和标准输出可以由freopen函数分配给其他的文件指针)

6 错误处理 - stderr和exit

12.14
cat对错误的处理还不是那么理想。有一个问题在于如果出于某些原因而有一个文件不能被访问,相关错误会打印在所有文件被打印的内容之后。这对于打印到屏幕上来说还可以接受,但如果是将这些内容输出到文件或通过管道输出到其它程序就有些不可接受了。

欲更好处理该问题,可以利用第二个名为stderr的输出流,它跟stdin和stdout以相同的方式分配给程序。往stderr中输出的内容往往会显示在屏幕上即使输出被重定向。

让我们一起来改写cat让其错误信息输出到标准错误中。

#include <stdio.h>/* cat:关联文件,版本2 */main(int argc, char *argv[]){    FILE    *fp;    void filecopy(FILE *, FILE *);    char *prog = arg[0];     /* 程序名 */    if (argc == 1) /* 无参数:复制标准输入 */        filecopy(stdin, stdout);    else        while (--argc > 0)            if ((fp = fopen(*++argv, “r”)) == NULL) {                fprintf(stderr, “%s: can’t open %s\n”,                    prog, *argv);                exit(1);            } else {                filecopy(fp, stdout);                fclose(fp);            }    if (ferror(stdout)) {        fprintf(stderr, “%s: error writing stdout\n”, prog);        exit(2);    }    exit(0);}

该程序以两种方式来表明错误。第一,错误信息由fprintf输出到stderr。程序在信息中包含了程序名即argv[0],所以对用户来说错误源比较明显。

第二,程序使用了标准库函数exit,调用exit时程序会被终止。exit的参数比较随意,该参数可以被其他程序用来判断该程序的运行是否成功。传统来讲,返回值0表示运行正常结束;非0值表示相应的出错情况。exit会调用fclose来关闭每个打开的输出文件,并将缓冲区的内容输出。

在main中,返回expr等效于exit(expr)。exit在其他的函数中曾有被调用过,如第五章的模式搜索程序。

如果在fp关联的流中发生了错误那么ferror将返回非0值。

int ferror(FILE *fp)

尽管输出错误比较少见,但它们的确存在(例如,磁盘被写满),所以程序应该检测这一点。
函数feof(FILE *)跟ferror类似;如果遇到文件结尾则返回非0值。

int feof(FILE *fp)

在比较小的程序中通常不同担心exit的参数值,但对于比较复杂的程序来说应该仔细考虑怎么用该参数值去代表相应的含义。

7 行输入输出

12.15
标准库函数提供了类似之前章节所使用的getline函数的功能的fgets:

char *fgets(char *line, int maxline, FILE *fp)

fgets读取fp所关联的文件中接下来的一行到字符数组line中;最多读取maxline – 1个字符。line末尾包含’\0’字符。fgets通常返回line;如果遇到文件末尾或遇到错误则返回NULL。(之前的getline函数返回文本行的长度,这是一个更有用的值;如果返回0则表示遇文件结束)

对于输出,fputs将写字符串(不需要包含换行符)到文件中:

int fputs(char *line, FILE *fp)

遇到错误时,该函数返回EOF;成功则返回一个非负数。

库函数gets和puts跟fgets和fputs相似,只是它们分别针对stdin和stdout。令人困惑的是,gets会删除末尾的’\n’,而puts又会将’\n’添上。

欲展示像fgets和fputs这样的函数其实没什么特别之处,我们将标准库中对它们的实现复制如下:

/* fgets: 从iop指向的文件中最多读取 n-1个字符,并在末尾补上 ‘\0*/char *fgets(char *s, int n, FILE *iop){    register int c;    register char *s;    cs  = c;    while (--n > 0 && (c = getc(iop)) != EOF)        if ((*cs++ = c) == ‘\n’)            break;    *cs = ‘\0’;    return (c == EOF && cs == s) : NULL : s;}/* fputc:将字符串s输出到iop涉及的文件中 */int fputs(char *s, FILE *iop){    int c;    while (c = *s++)        putc(c, iop);    return ferror(iop) ? EOF : non-negative;}

标准库指定ferror返回错误值;当错误发生时,fputs返回EOF;没有错误发生时,它返回非负值。

如此一来,用fgets来实现之前的getline就比较简单了:

/*getlien: 读取一行,并返回其长度 */int getline(char *line, int max){    if (fgets(line, max, stdin) == NULL)        return 0;    else        return strlen(line);}

练习 7-6。编写程序来比较两个文件,打印首次不同的行。
练习 7-7。修改第五章的模式查找程序,让其首先从文件中查找,如果没有文件从标准输入中查找。当匹配一行时应该将文件名打印出来么?
练习 7-8。编写打印一系列文件的程序,没打印一个文件就从一个新页开始,并且带上标题并附加上页码。

8 函数集

12.19
标准库提供了各种各样的函数。本节是一个最常用的简介。更多细节和其他许多函数可以查看附录B。

8.1 字符串操作
关于字符串相关的函数,我们已经提到了strlen,strcpy,strcat和strcmp,他们都在string.h头文件中。下列涉及的参数s和t都是char *类型,c和n都是int类型。

strcat(s, t)    // 将t连接到s的末尾strncat(s, t, n)    // 将t的前n字符连接到s的末尾strcmp(s, t)    // 如果 s < t则返回负数,s == t则返回0,s > t则返回正数strncmp(s, t, n)    // 比较s和t的前n个字符strcpy(s, t)        // 将t指向的字符串拷贝到sstrncpy(s, t, n)        // 最多拷贝t的的n个字符到s中strlen(s)           // 返回s的长度strchr(s, c)        // 返回字符c在s中首次出现的地址,如果c不在s中则返回NULLstrrchr(s, c)       // 返回c在s中最后出现的地址,如果c不在s中则返回NULL

8.2 字符类测试和转换
在ctype.h中的几个函数可以测试字符并对其进行转换。在下列函数中,c是一个int,它被当成unsigned char类型或者EOF。这些函数都返回int。

isalpha(c)      // 如果c是字母则返回非0,否则返回0isupper(c)  // 如果从是大写则返回非0,否则返回0islower(c)      // 如果c是小写则返回非0,否则返回0isdigit(c)      // 如果c是数字则返回非0,否则返回0isalnum(c)  // 如果c是字母或者数字则返回非0,否则返回0isspace(c)      // 如果c是空白,制表符,换行符,回车符,换页符或者垂直制表符则返回非0,否则返回0toupper(c)  // 将c转换为大写字母并返回tolower(c)  // 将c转换为小写字母并返回

8.3 ungetc
标准库提供了一个颇具约束的函数ungetch,正如第四章所写;该函数调用了ungetc。

int ungetc(int c, FILE *fp)

将字符c压回fp所关联的文件中,并返回c,或者返回EOF来表示错误。每次只保证输入一个字符到文件中。ungetc可以像scanf、getc或getchar这样的函数使用。

8.4 命令
函数system(char *s)执行包含在字符串s中的命令,然后再继续执行该语句所在的程序。s的内容依赖特定的操作系统。列举一个在UNIX系统上的例子,语句

system(“date”);

将会运行date程序;它打印某天的日期和时间到标准输出。system返回的返回值基于特定系统中所执行的命令。在UNXI系统中,所返回的状态为调用exit时所给的参数值。

8.5 内存管理
函数malloc和calloc能动态的获得一块内存。

void    *malloc(size_t n)

malloc返回一个指向n字节未初始化的指针,如果分配失败则返回NULL。

void *calloc(size_t n, size_t size)

该函数以指定大小size为基本单位并分配n个,如果分配失败则返回NULL。由该函数分配的内存被初始化为0。

由malloc和calloc返回的指针所指向的对象有适当的对齐要求问题,返回的指针需要被强制转换为相应的类型,如

int *ip;ip  = (int *)calloc(n, sizeof(int));

free(p)将释放由p所指向的内存空间,p由malloc和calloc返回。对内存空间的释放顺序没有严格的要求,但释放非由malloc或calloc分配的内存将会有很恐怖的错误。

使用已经被释放的指针也会有错误。下面就是一段典型但不对的代码,循环是在释放链表中的结点:

for (p = head; p != NULL; p = p->next) /*错误*/    free(p);

正确的方式是提前保存释放前所需要的指针:

for (p = head; p != NULL; p = q) {    q   = p->next;    free(p);}

8.7节将会展示像malloc这样的存储分配器,它分配的块可以按照顺序被释放。

8.6 数学函数
在math.h中有二十多个数学函数;以下是一些经常被使用的。每个函数将会需要1个或者2个double类型的参数并返回一个double。

sin(x)  // x的sine,x为弧度cos(x)  // x的cosine,x为弧度atan2(y, x) // y / x的arctangent,以弧度exp(x)      // 指数函数e^xlog(x)      // x(x >0)的自然对数(基数为e)log10(x)        // x(x >0)的常用对数(基数为10)pow(x, y)       // x^ysqrt(x)     // x(x >= 0)的开方fabs(x)     //x的绝对值

8.7 随机数的生成
函数rand()将会产生0到RAND_MAX的伪随机整数,该函数定义在stdlib.h中。产生大于或等于0但小于1的浮点数的一种方法为

#define frand() ((double) rand() / (RAND_MAX + 1.0))

(如果库已经提供了产生浮点数的函数,它可能比以上这个具有更好的统计性。

函数srand(unsigned)为函数rand设置种子。rand和srand的可移植实现可以参见2.7节。

练习 7-9。像isupper这样的函数可以保存空间或时间。尝试实现。

第八章:UNIX系统接口

12.20
UNIX操作系统通过系统调用提供了服务,它们是操作系统内可被用户程序调用的有效函数。本节描述在C程序中如何使用一些重要的系统调用。如果您正在使用UNXI操作系统,那么对学习本节内容有直接的帮助,有的时候为了达到最大效率或者要访问一些库中不包含的工具函数就有必要使用系统调用。然而,即使您在不同的操作系统之上使用C语言,您也可以通过对本章例子的学习而加深对C程序的认识;尽管对于不同的操作系统上细节的实现会有变化,但相似的代码能在任何操作系统中找到。因为ANSI C库很多都是学习的UNIX工具的模型,这些代码也能帮助您理解库。

本章分为三个主要部分:输入/输出,文件系统以及存储管理。前两部分使用UNXI系统的外部特性。

第七章讨论的输入/输出接口是不同操作系统都具有的同一格式。在特定的操作系统上,每个库函数的实现要结合主机来编写。在接下来的几节中,我们会讨论UNXI系统上的输入/输出的系统调用,并展示部分库函数是怎么通过系统调用而被实现的。

1 文件描述符

在UNIX操作系统中,所有输入和输出都是通过读或写文件完成的,因为所有的外围设备,甚至键盘和屏幕在文件系统中都是文件。这意味着是一种同类型的接口在操作程序和外围设备的通信。

在大多通用的情况,在读或写一个文件之前,必须打开文件的意图先告知系统。如果欲在文件中执行写操作,那么可能还有创建该文件或清除该文件之前的内容。系统会合理地检查这些操作(文件存在否?有访问文件的权限否?),如果所有都通过,那么就返回一个名为文件描述符的非负小整数给程序。对文件的输入或输出不管在什么时候完成,文件描述符都被用来代替文件名。(文件描述符跟标准库中所使用的文件指针或跟MS-DOS下的文件句柄类似)所有跟打开文件相关的信息都有系统维护;用户程序跟文件相关的只有文件描述符。

由于输入和输出普遍地涉及到键盘和屏幕,为更加便利,对此有特定的设定。当命令行翻译器(shell)运行一个程序时,有三个文件是打开的,它们的文件描述符分别为0,1,2,它们分别被称为标准输入,标准输出,标准错误。如果程序读0并写1和2,就可以在不必担心打开文件的步奏就可实现输入和输出。

程序的用户可以通过使用 < 或 > 来重定向I/O到文件或文件到I/O:

prog     <infile >outfile

在这种情况下,shell会改变为描述符0和1默认安排而将它们分别作为infile和outfile的文件描述符。通常文件描述符2也是跟屏幕关联的,所以错误信息可以输出到屏幕上。输入或输出也可以跟管道关联。在所有的情况下,文件的分配有shell改变而不是由程序改变。只要程序使用文件0来作输入并使用1和2作为输出,那么程序并不知道其输入来自哪里也不知道其输出会到哪里。

2 较底层的I/O - read和write

12.27
输入和输出使用read和write系统调用,在C程序中通过两个函数调用read和write可以访问输入输出。对二者来说,第一个参数都是文件描述符。第二个参数是程序中保存数据去向或来源的字符数组。第三个参数需要被转传送的字节个数。

int n_read = read(int fd, char *buf, int n);int n_written = write(int fd, char *buf, int n);

每次调用将返回所传输的字节数。读时,返回的字节数可能会比参数要求的要少。返回0则表示到了文件末尾,出错则返回-1。写时,返回值是所写成功的字节数;如果返回值不等于写时所要求的字节数则是遇到了某种错误。

在一次调用中可以读/写任何字节数。最常见的值为1,该值意味着一次读/写一个字符(无缓冲),像1024或4096这样的数字则表示某个设备的一个特定块。越大的数字将意味着更高效,因为这样会让更少的系统调用发生。

将这些效果结合起来,我们可以编写一个复制输入到输出的简单程序,跟第一章所编写的文件复制程序等效。该程序可以复制来自任何地方的东西到任何地方去,因为输入和输出可以被重定向到文件或设备。

#include    “syscalls.h”main() /* 复制输入到输出 */{    char buf[BUFSIZE];    int n;    while ((n = read(0, buf, BUFSIZE)) > 0)        write(1, buf, n);    return 0;}

我们将系统调用的函数原型收集到名为syscalls.h的头文件中,所以我们可以在本章中将该头文件包含在程序中。但该头文件并不是标准头文件。

参数BUFSIZE也定义在syscalls.h中;它的值对本地操作系统合适。如果文件从大小不是BUFSIZE的倍数,靠后的某个调用的read将会返回更小一些的字节数共wirte进行写操作;下一次再调用read时将返回0。

用read和write构建像getchar、putchar等稍高层的程序比较有说明性。例如,以下有一个屋缓冲输入版本的getchar,每次从标准输入中读取一个字符。

#include “syscalls.h”/* getchar: 单个字符输入 */int getchar(void){    char c;    return (read(0, &c, 1) == 1) ? (snsigned char) c : EOF);}

c必须是char,因为read需要字符指针。在函数返回时将c转换为unsigned char能避免符号扩展问题。

getchar的第二个版本将处理大块的输入,并将这些字符一次性读入某缓冲区中。

#include “syscalls.h”/* getchar: 简单的缓冲版本 */int getchar(void){    static char buf[BUFSIZE];    static char *bufp = buf;    static int n = 0;    if (n == 0) {   /* 缓冲区为空 */        n   = read(0, buf, sizeof buf);        bufp = buf;    }    return (--n >= 0) ? (unsigned char) *bufp++ : EOF;}

如果要将getchar的这些版本编译并包含在stdio.h中,就需要用#undef来将用宏实现的getchar版本屏蔽。

12.29
除了默认的标准输入、输出和错误外,必须准确的打开某文件以供读写。有oepn和creat两个系统调用可以用作打开文件。

open跟第七章讨论的fopen不同,该函数不是返回一个文件指针,而是返回一个文件描述符,该文件描述符是一个int。如果出现错误,open会返回-1。

#include <fcntl.h>int fd;int open(char *name, int flags, int perms);fd  = open(name, flags, perms);

跟fopen的第一个参数一样,name是一个包含文件名的字符串。第二个参数flags是指定如何打开文件的整型;其主要值为

O_RDONLY    以只读方式打开O_WRONLY    以只写方式打开O_RDWR      以读和写的方式打开

这些常量在System V UNIX 操作系统中的fcntl,h被定义,在Berkeley(BSD)版本中被定义在sys/file.h文件中。

打开一个已经存在的文件供读操作,

fd  = open(name, O_RDONLY, 0);

perms参数在我们讨论open的使用过程中始终为0。

试图打开一个不存在的文件是一个错误。系统调用creat能创建一个新文件,也能重写旧的文件。

int reate(char *name, int perms);fd  = creat(name, perms);

如果成功创建文件则返回一个文件描述符,否则返回-1。如果文件已存在,creat将会把文件截断为0长度,并消除其之前的内容;创建一个已经存在的文件并不会有错误。

如果文件确实不存在,creat将以perms参数指定的权限创建它。在UNIX文件系统中,提供9个位来提供权限信息,以让文件、文件组所有者或其他用户控制读、写以及执行权限。因此,使用3位8进制数可方便指定该权限。例如,0755指定文件的所有者有读、写以及执行权限,对于其他用户只有读和执行的权限。

以下有一个对UNIX cp程序的简单版本,cp将一个文件的内容拷贝到另外一个文件中。以下版本只复制一个文件,它不允许第二个参数为目录,它会创造权限而非复制它们。

#include <stdio.h>#include <fcntl.h>#include “syscalls.h”#define PERMS   0666    //所有者、所有者组及其他用户拥有读写权限void error(char *, …);/* cp: 复制f1 到 f2 */main(int argc, char *argv[]){    int f1, f2, f3;    char buf[BUFSIZE];    if (argc != 3)        error(“Usage:cp from to”);    if ((f1 = open(argv[1], O_RDONLY, 0)) == -1)        error(“cp:  can’t open %s”, argv[1]);    if ((f2 = creat(argv[2], PERMS)) == -1)        error(“cp:  can’t create %s, mode %03o”, argv[2], PERMS);    while ((n = read(f1, buf, BUFSIZE)) > 0)        if (write(f2, buf, n) != n)            error(“cp: write error on file %s”, argv[2]);    return 0;}

该程序用第8.6节描述的stat系统调用以权限0666来创建输出文件。我们可以判断存在文件的模型,因为给以该模型以拷贝。

注意error是诸如printf有可变参数的函数。error是如何使用printf家族的另一个演示。除开它的可变参数列表由va_start宏初始化外,vprintf跟printf类似。同理,vfprintf和vsprintf跟fprintf和sprintf类似。

#include <stdio.h>#include <stdarg.h>/* error: 打印错误信息并结束程序 */void error(char *fmt, …){    va_list args;    va_start(args, fmt);    fprintf(stderr, “error: “);    vfprintf(stderr, fmt, args);    fprintf(stderr, “\n”);    va_end(args);    exit(1);}

允许一个程序同时打开文件的数量有一定的限制(大约20个)。因此,任何试图处理多个文件的程序必须准备重复使用文件描述符。函数close(int fd)将切断文件描述符和打开文件的联系,并释放文件描述符fd给其他文件;它跟标准库文件中的fclose类似,只是它没有缓冲刷新。在main中通过exit或return终止程序将会关闭所有的文件。

函数unlink(char *name)将会从文件系统中移除所有名为name的文件。标准库中跟此函数关联的是remove函数。

练习 8-1。用read、write、open或close代替标准库中等价的函数重新编写cat程序。比较两个不同版本的cat的执行速度。

4 随机访问 - lseek

12.30
输入和输出通常是相继的:read或write在源文件中刚好在另一个之前或之后发生。然而,当有必要时,可以任何顺序读写一个文件。系统lseek提供到文件任意位置的方法而不用读或写任何数据:

 long lseek(int fd, long offset, int origin);

该函数将文件描述符为fd的文件的当前位置设置为offset处,offset以origin位置为起始点。后续的读或写将从当前位置开始。origin可以为0,1或2以指定offset以文件开始位置、当前位置或从文件末尾处为参考。例如,在文件中附加内容(UNXI shell中的重定向符或fopen中的”a”参数),就可以在写操作之前将当前位置设置到文件末尾:

 lseek(fd, 0L, 2);

回到文件的开始处使用:

 lseek(fd, 0L, 0);

注意0L这个参数;这个参数可以写为(long) 0或0,如果lseek被正确声明的话。

有了lseek,就可以或多或少将文件当成一个大数组对待,当然要以更慢的访问速度为代价。例如,以下函数从文件任意位置读取任意字节内容。该函数返回所读得字节数,出错则返回-1。

#include "syscalls.h"/* get:从位置pos处读取n字节内容 */int get(int fd, long pos, char *buf, int n){    if (lseek(fd, pos, 0) >= 0) // 获取文件位置        return read(fd, buf, 0);    else        return -1;}

lseek的返回值为long,是在文件中设置的新位置,如果出错则返回-1。标准库函数fseek跟lseek类似,只是前者的第一个参数为FILE *且当有错误发生时返回值为非0。

5 例 - fopen和getc的实现

本节通过实现标准程序fopen和getc来演示如何将系统调用的这些特性整合在一起。

标准库是用文件指针而非用文件描述符来跟文件关联。文件指针是指向一个包含文件相关信息的指针,该结构体中包含:指向缓冲区的指针,这使得文件可以较大块进行读写;一个计数缓冲区还剩余的字符数;指向缓冲区下一个字符的指针;文件描述符;以及描述读写模型的标志,错误状态等。

描述文件的数据结构包含在stdio.h中,任何一个使用标准输入/输出库的源文件都必须使用#include包含该头文件。该头文件也包含了其他的函数。以下内容是stdio.h典型的摘录,命名以下划线开头是为了降低跟用户程序中的命名冲突的可能性。这成为所有标准库程序的传统。

#define NULL        0#define EOF         (-1)#define BUFSIZ      1024#define OPEN_MAX    20  /* 能同时打开文件的最大数量*/typedef struct _iobuf {    int cnt;    /* 余下的字符数 */    char *ptr;  /* 下一个字符的位置 */    char *base; /* 缓冲区的位置 */    int falg;   /* 文件访问模式 */    int fd;     /* 文件描述符 */} FILE;extern FILE _iob[OPEN_MAX];#define stdin   (&_iob[0])#define stdout  (&_iob[1])#define stderr  (&_iob[2])enum _falgs {    _READ = 01,     /* 打开文件的读模式 */    _WRITE = 02,    /* 打开文件的写模式 */    _UNBUF = 04,    /* 文件无缓冲 */    _EOF = 010,     /* 在文件中遇到EOF */    _ERR = 020      /* 在文件中发生错误 */};int _fillbuf(FILE *);int _flushbuf(int, FILE *);#define feof(p)     (((p)->flag & _EOF) != 0)#define ferror(p)   (((p)->flag & _ERR) != 0)#define fileno(p)   ((p)->fd)#define getc(p)     (--(p)->cnt >= 0    \                ? (unsigned char) *(p)->ptr++ : _fillbuf(p))#define putc(x, p)  (--(p)->cnt >= 0    \                ? *(p)->ptr++ = (x) : _flushbuf((x), p))#define getchar()   getc(stdin)#define putchar(x)  putc((x), stdout)

getc宏通常会递减计数,增加指针计数,并返回字符。(回想用反斜扛线定义的长长的那个宏)。如果计数变为了负数,getc就会调用_fillbuf重新补充缓冲区、重新初始化结构体内容并返回一个字符。该字符以unsigned的形式返回,这样可以确保所有的字符都为正。

尽管没有讨论任何细节,我们已经包含了put的定义以展示跟getc类似的操作方式,当缓冲区满时它调用_flushbuf来输出缓冲区。同时还包含了访问错误和文件结束状态以及文件描述符的宏。

那么,就可以着手编写函数fopen了。fopen的主要功能是将文件打开并设置文件的正确位置,并设置标志位来表示文件相应的状态。fopen不会分配任何的缓冲区空间;当第一次读文件时,该功能由_fillbuf完成。

 #include <fcntl.h> #include "syscalls.h" #define PERMS 0666 /* 拥有者、组以及其他用户有读写文件的权限 */ /* fopen: 打开文件,返回文件指针 */ FILE *fopen(char *name, char *mode) {     int fd;     FILE *fp;    if (*mode != 'r' && *mode != 'w' && *mode != 'a')        return NULL;    for (fp = _iob; fp < _iob + OPEN_MAX; fp++)        if ((fp->flag & (_READ | _WRITE)) == 0)            break; /* 找到未使用的位置 */    if (fp >= _iob + OPEN_MAX) /* 无空闲位置 */        return NULL;    if (*mode == 'w')        fd = creat(name, PERMS);    else if ('a') {        if ((fd = open(name, O_WRONLY, 0)) == -1)            fd = creat(name, PERMS);        lseek(fd, 0L, 2);    } else        fd = open(name, O_RDONLY, 0);    if (fd == -1) /* 不能访问name */        return NULL;    fp->fd = fd;    fp_cnt = 0;    fp->base = NULL;    fp->flag = (*mode == 'r') ? _READ : _WRITE;    return fp;}

该版本的fopen没有处理标准库中所提供的fopen版本所有的访问模式,尽管完成这些模式并不会增加太多代码。特别得是,此处的fopen不识别”b”(二进制访问),因为该模式在UNIX系统上无意义,也没有识别同时意味着读和写的”+”模式。

首次调用getc会发现计数为0,这将会强制的调用_fillbuf。如果_fillbuf发现文件没有以读模式打开,它就会立即返回EOF。否则,它就会尝试分配一块缓冲区(如果读支持缓冲模式)。

缓冲一旦建立,_fillbuf会调用read来填充缓冲区,并设置计数和指针,并返回缓冲区开始处的字符。后续调用_fillbuf将会找到该缓冲区。

#include "syscalls.h"/* _fillbuf:分配并填充输入缓冲区 */int _fillbuf(FILE *fp){    int bufsize;    if ((fp->flag & (_READ | EOF | _ERR)) != _READ)        return EOF;    bufsize = (fp->flag & _UNBUF) ? 1 : BUFSIZ;    if (fp->base == NULL) /* 还未有缓冲区 */        if ((fp->base = (char *)malloc(bufsize)) == NULL)            return EOF; /* 分配缓冲区失败 */    fp->ptr = fp->base;    fp->cnt = read(fp->fd, fp->ptr, bufsize);    if (--fp->cnt < 0) {        if (fp->cnt == -1)            fp->flag |= _EOF;        else            fp->flag |= _ERR;        fp->cnt = 0;        return EOF;    }    return (unsigned char) *fp->ptr++;}

剩下的问题就是怎么让这一切开始。数组_iob必须定义并被stdin,stdout以及stderr初始化:

 FILE _iob[OPEN_MAX] = { /* stdin, stdout, stderr: */     { 0, (char *) 0, (char *) 0, _READ, 0 },     { 0, (char *) 0, (char *) 0, _WRITE, 1 },     { 0, (char *) 0, (char *) 0, _WRITE | _UNBUF, 2}};

对结构体flag部分初始化后的结果是stdin供读,stdout供写,stderr以无缓冲模式写。

练习 8-2。用位域代替位操作重新编写fopen和_fillbuf。比较二者的代码量和执行速度。
练习 8-3。设计并编写_flushbuf,fflush以及fclose。
练习 8-4。标准库函数

 fseek(FILE *fp, long offset, int origin)

跟lseek相似,只是它的第一个参数是文件指针且其返回值是一个整型而不是文件位置。编写fseek。确保fseek能和库中的其他函数协调使用缓冲区。

6 例 - 目录列表

01.08
有时会有一种不同的文件系统交互会需要判断一个文件的某信息,而不是该文件所包含的内容。像UNIX系统上的ls就是一个例子 —— 它选择性地打印目录中文件的大小、权限以及其他一些信息。MS-DOS下的dir命令也有类似功能。

因为UNIX的目录也是文件,ls就只需要读取它并检索其文件名。但是有必要调用系统调用来获取文件诸如大小的信息。在其他系统上,甚至还需要使用系统调用来获取文件名;MS-DOS就是一个例子。我们的目标是以一种相对独立于具体系统的方式来访问信息,即使其实现细节基于不同系统而不同。

我们会编写一个名为fsize的程序来演示这一点。fsize是ls的一个特殊版本,它根据其命令行参数打印所有命名文件的大小。如果有文件为目录,fsize将会自动递归该目录。如果没有给fsize参数,那么它就默认处理当前目录。

我们以简单学习一下UNIX文件系统结构作为开始。目录是包含所有文件名列表并能指示这些文件位置的文件。“位置”是被称为“结点列表”的另外一张表的索引。文件的结点包含了除文件名之外的所有信息。目录入口通常由两项组成,文件名和结点号。

不过,目录的格式和其精确的内容在不同版本的系统上有所不同。所以我们将任务分成两部分以将不可移植的部分独立。外层定义了不基于特定系统能访问文件名和结点号的Dirent的结构体和opendir、readdir和closedir三个函数。然后我们再演示使用相同的结构如何在Version 7系统和System V UNIX上具体实现。其他系统上的实现留作练习。

Dirent结构体包含结点编号和命名。文件名的最大长度为NAME_MAX,该值基于特定的系统。opendir返回指向名为DIR结构体的指针,该结构体跟FILE结构体类似,该指针被readdir和closedir使用。这些信息被集中在dirent.h文件内。

#define NAME_MAX    14  /* 允许的最常的文件名长度;该值基于特定系统 */typedef struct {    /* 可移植的目录入口 */    long ino;       /* 结点编号 */    char name[NAME_MAX + 1]; /* name + '\0'终止符 */} Dirent;typedef struct {    /* 最小结构体的DIR:无缓冲等 */    int fd;         /* 目录的文件描述符 */    Dirent d;       /* 目录入口 */} DIR;DIR *opendir(char *dirname);Dirent *readdir(DIR *dfd);void closedir(DIR *dfd);

系统调用stat将会根据文件名返回该文件结点的所有信息,如果失败则返回-1。即

 char *name; struct stat stbuf; int stat(char *, struct stat *); stat(name, &stbuf);

将会用文件名为name的文件的结点信息填充stbuf结构体。文件sys/stat.h中描述了由stat返回的结构体,该结构体典型的样子为:

 struct stat /* 由stat返回的结点信息 */ {    dev_t    st_dev; /* 设备的结点 */    ino_t    st_ino; /* 结点编号 */    short    st_mode; /* 模式位 */    short    st_nlink; /* 文件的链接数 */    short    st_uid;   /* 拥有者的用户id */    short    st_gid;   /* 拥有者的组id */    dev_t    st_rdev;  /* 为特殊的文件使用 */    off_t    st_size;  /* 以字节为单位,文件的大小 */    time_t   st_atime; /* 最后被访问的时间 */    time_t   st_mtime; /* 最后被修改的时间 */    time_t   st_ctime; /* 结点最后被修改的时间 */} ;

大多数值由注释解释了。像dev_t和ino_t被定义在sys/typed.h中,必须包含该头文件。

大多数值由注释解释了。像dev_t和ino_t被定义在sys/typed.h中,必须包含该头文件。

st_mode项包含了一套描述文件的标志。这些标志的定义也在sys/stat.h文件中;我们只需要列出该标志的一部分即可:

#define S_IFMT  0160000 /* 文件类型 */#define S_IFDIR 0040000 /* 目录 */#define S_IFCHR 0020000 /* 特殊字符 */#define S_IFBLK 0060000 /* 特殊块 */#define S_IFREG 0100000 /* 规整 *//* ... */

现可即手编写fsize程序了。如果从stat获取到的模式只是该文件不是目录,那么文件的大小就可以打印。然而如果文件是目录,就需要将目录依次将目录处理为一个文件;他可以依次包含子目录,所以该处理是递归的。

主程序处理命令行参数;它处理函数fsize的每个参数。

#include <stdio.h>#include <string.h>#include "syscalls.h"#include <fcntl.h>  /* 读和写 */#include <sys/types.h>  /* 类型定义 */#include <sys/stat.h>   /* 由stat返回的结构体 */#include "dirent.h"void fsize(char *);/* 打印文件大小 */main(int argc, char **argv){    if (argc == 1) /* 默认:当前目录 */        fsize(".");    else        wihle (--argc > 0)            fsize(*++argv);    return 0;}

fsize函数打印文件的大小。如果文件是目录,fsize首先调用dirwalk来处理其内的所有文件。注意sys/stat.h中的S_IFMT和S_IFDIR用来判断是文件还是目录。注意括号,因为 & 的优先级低于 == 。

 int stat(char *, struct stat *); void dirwalk(char *, void (*fcn)(char *)); /* fsize:打印名为 name 文件的大小 */ void fsize(char *name) {     struct stat stbuf;    if (stat(name, &stbuf) == -1) {        fprintf(stderr, "fsize: can't access %s\n", name);        return ;    }    if ((stbuf.st_mode & s_IFMT) == S_IFDIR)        dirwalk(name, fsize);    printf("%8ld %s\n", stbuf.st_size, name);}

dirwalk函数是一个将函数应用于目录内所有文件的通用函数。它打开该目录,并循环遍历该目录内的文件,对每个文件调用该函数,然后关闭该目录并返回。因为fsize对每个目录都调用dirwalk,所有这两个函数是相互递归的。

#define MAX_PATH    1024/* dirwalk: 对目录dir中的所有文件进行 fcn函数 操作 */void dirwalk(char *dir, void (*fcn)(char *)){    char name[MAX_PATH];    Dirent *dp;    DIR *dfd;    if ((dfd = opendir(dir)) == NULL) {        fprintf(stderr, "dirwalk: can't open %s\n", dir);        return ;    }    while ((dp = readdir(dir)) != NULL) {        if (strcmp(dp->name, ".") == 0            || strcmp(dp->name, "..") == 0)                    continue;/* 跳过自身和父目录 */        if (strlen(dir) + strlen(dp->name) + 2 > sizeof(name))            fprintf(stderr, "dirwalk: name %s%s too long\n",                dir, dp->name);        else {            sprintf(name, "%s/%s", dir, dp->name);            (*fcn)(name);        }    }    closedir(dfd);}

每调用readdir就会返回指向下一个文件信息的指针,如果没有文件则返回NULL。每个目录都会包含自身项即”.”以及其父目录即”..”;这些都必须跳过,不然程序将会进入死循环。

到这个层次,这些代码不基于目录是什么格式的。下一步就是要在特定的系统上呈现最小版本的opendir、readdir以及closedir。后续程序基于Version 7和System V UNIX系统;它们使用sys/dir.h中定义的目录信息;其内容大概入下:

#ifndef DIRSIZ#define DIRSIZ  14#endifstruct direct /* 目录项 */{    ino_t d_ino; /* 结点编号 */    char d_name[DIRSIZ]; /* 无'\0'的最长命名 */} ;

系统的有些版本允许更长的命名并有着更为复杂的目录结构。

ino_t是描述结点列表中的索引的类型定义。它常是unsigned short,但这并没有嵌入到程序中;在不同的系统上不同,所以使用typedef更好。一套完整的系统定义在sys/type.h文件中。

opendir打开目录,验证文件是否为目录(这次调用系统调用fstat,它跟stat类似,只是它应用于文件描述符)、分配目录结构体并记录信息:

 int fstat(int fd, struct stat *); /* opendir: 问readdir调用打开目录 */ DIR *opendir(char *dirname) {     int fd;     struct stat stbuf;     DIR *dp;    if ((fd = open(dirname, O_RDONLY, 0)) == -1        || fstat(fd, &stbuf) == -1        || (stbuf.set_mode & S_IFMT) != S_IFDIR        || (dp = (DIR *)malloc(sizeof(DIR))) == NULL)        return NULL;    dp->fd= fd;    return dp;}

closedir关闭目录文件并释放空间:

 /* closedir:关闭由opendir打开的目录 */ void closedir(DIR *dp) {     if (dp) {         close(dp->fd);         free(dp);    }}

最后,readdir使用read对每个目录项。如果目录位置当前没有被使用(因为文件已经被移除),结点编号为0,位置也被跳过。否则,结点编号和命名被置于static的结构体中并返回给用户使用的指针。每次调用都会重写之前的信息。

#include <sys/dir.h> /* 本地的目录结构 *//* readdir:按照顺序读取目录项 */Dirent *readdir(DIR *dp){    struct direct dirbuf; /* 本地的目录结构体 */    static Dirent d;   /* 返回:可移植结构体 */    while (read(dp->fd, (char *)&dirbuf, sizeof(dirbuf))                == sizeof(dirbuf)) {        if (dirbuf.d_ino == 0) /* slot未使用 */            continue;        d.ino = dirbuf.d_ino;        strncpy(d.name, dirbuf.d_name, DIRSIZE);        d.name[DIRSIZ] = '\0'; /* 确定终止符 */        return &d;    }    return NULL;}

尽管fsize程序相当特别,但它确实演示了一些相当重要的思维。第一,许多程序并不是“系统程序”;它们仅是使用操作系统所维持的信息而已。对于这类程序,关键是要呈现只在标准头文件呈现的部分,程序包含这些标准库的内容即可。第二,用基于系统的接口创建相对独立于系统的接口时要上心。标准库中的函数就是很好的例子。

练习 8-5。修改fsize程序打印包含在结点项的其他信息。

7 例 - 存储分配器

01.09
在第五章呈现了一个基于栈的非常有限的存储器管理程序。本节所要编写的存储器程序版本将不会受到类似的严格限制。可以对malloc和free以任何顺序调用;当调用malloc时,如果有必要,能从操作系统获得更多的内存。该程序还演示了在特定系统上编写不依赖具体系统的编程方法,同时还展示了结构体、联合以及typedef的实际应用。

malloc不是从经编译而获得固定大小的数组处获得使用空间,而是根据需要从操作系统处获得相应的内存空间。由于程序中的其他活动也可能通过其他方式获取内存空间,所以malloc管理的内存空间可能不是连续的。所以其空闲空间以列表块的形式存在。这些块以地址递增的顺序按序被保存,最后一块(最高地址)指向第一块。
这里写图片描述

当请求内存空间时,依次浏览空闲的内存块直到找到足够大的块。该算法称为“首次适应”,与之相比的是“最佳适应”,该算法会寻找与请求块差距最小的块。如果请求块跟某空闲块的大小刚好吻合,那么就将该块从列表中移除并返回给用户。如果寻找的块太大,则从该块中分割出符合请求块大小的块给用户,将分割剩下的块继续保存在列表中。如果没有找到足够大的块,将会向操作系统回去一个大块并将其添加到列表中来。

释放内存时也会搜索该空闲列表,以找到合适的地方插入所释放的内存块。如果释放块跟某空闲块相邻,那么它们就组合成一个更大的块,这样就可以避免内存碎片加大化。由于空闲列表以地址递增的顺序呈现,所以查找比较方便。

在第五章间接提到的那个问题,即确定由malloc返回的内存在存数据时以合适的大小对齐。尽管机器各异,但每个机器都有最严格的类型:假设最严格的类型能够存储在一个特定的地址中,其他的类型也就都能够被存储了。在一些机器上,最严格的类型是double;而在另一些机器上,最严格的类型是int或long。

空闲块包含了指向链中的下一个空闲块的指针、保存该块大小的记录以及该块的空闲空间;开头的控制信息称为“头”。欲简化对其,所有的块都是头的整数倍,然后将头以合适的方式对齐。这可以通过使用联合来包含头结构和最严格的对齐类型,在这里将最严格的对齐类型选择为long:

 typedef long Align;    /* 以long类型为边界对其 */ union header { /* 块头 */     sturct {         union header *ptr; /* 空闲列表中的下以空闲块 */         unsigned size;     /* 本块的大小 */    } s;    Align x; /*对块进行强制对齐 */} ;typedef union header Header;

Align与不会被使用;它只是用来以最坏的边界对齐来让每个头对齐。

在malloc中,所请求的字节数将会向上舍入到成为头大小的字节数;该块将会分配包含更多单元的内存,有头、记录本块大小的域。由malloc返回的指针指向空闲空间,并不是指向头。用户可以使用该空闲空间存储任何数据,但如果访问了空闲之外的空间将可能会引起混乱。
这里写图片描述
size域是必要的,因为由malloc控制的块不一定是连续的 —— 由指针运算可能计算不了该大小。

变量base用来开始。第一次调用malloc时,freep为NULL,那么空闲列表就被创建;它包含大小为0并指向自己的块。在任何情况下,空闲块就在接下来被搜索。该搜索从起始点(freep)处开始直到搜索到大小足够的块或直到最后一个块;该策略能够让列表均匀化。如果找打一个过大的块,就将其尾部返回给用户;在这种方式下,原来的头仅需要调整其尺寸。在所有的情况下,返回给用户的指针指向块内的空闲块,该空闲块紧紧连在头之后。

 static Header base; /* 用于开始的空表 */ static Header *freep = NULL; /* 空闲表的起点 */ /* malloc:通用的存储分配器 */ void *malloc(unsigned nbytes) {     Header *p, *prevp;     Header *morecore(unsigned);     unsigned nunits;    nunits = (nbytes + sizeof(Header) - 1) / sizeof(Header) + 1;    if ((prevp = freep) == NULL) { /* 还无空闲列表 */        base.s.ptr = freep = prevp = &base;        base.s.size = 0;    }    for (p = prevp->s.ptr; ; prevp = p, p = p->s.ptr) {        if (p->s.size >= nunits) { /* 足够大 */            if (p->s.size == nunits) /* 刚好 */                prevp->s.ptr = p->s.ptr;            else { /* 分配末尾部分 */                p->s.size -= nunits;                p += p->s.size;                p->s.size = nunits;            }            freep = prevp;            return (void *)(p + 1);        }        if (p == freep)            if ((p = morecore(nunits)) == NULL)                return NULL; /* 没有剩下的了 */    }}

函数morecore从操作系统处获取存储空间。该函数的细节依不同的系统而不同。因为向操作系统请求内存是一个相当昂贵的操作,所以我们并不想每次调用malloc时都有这个操作,而让每调用依次morecore都至少请求NALLOC单元内存;该块足够满足所申请内存块的大小。在设置size域后,morecore将调用free将额外的内存插入空闲列表中。

UNIX系统调用sbrk(n)返回一个指向额外的n字节空间的指针。如果无多余空间返回sbrk就会返回-1,哪怕返回NULL也比返回-1好。-1必须被强制转换为char *,这样才能成为该系统调用的返回值。强制类型转换能够避免不同机器上指针所代表的不同细节。然而,这里仍有一个假设,由sbrk返回的指向不同的块的指针的比较是有意义的。这不被标准所保证,标准允许指向同一块内存的指针比较。因此,该版本的malloc可能只能在对通用指针的比较有意义的机器上移植。

#define NALLOC 1024 /* 向操作系统请求的最小单元数 *//* morecore: 向系统请求更多的内存 */static Header *morecore(unsigned nu){    char *cp, *sbrk(int);    Header *up;    if (nu < NALLOC)        nu = NALLOC;    cp = sbrk(nu * sizeof(Header));    if (cp == (char *) -1) /* 无空间 */        return NULL;    up = (Header *) cp;    up->size = nu;    free((void *)(up + 1);    return freep;}

free成为最后一件事情了。它将从freep开始浏览空闲表,在表中查找地方插入空闲块。可能会插入到两块之间或插入到表的末尾。在任何情况下,如果释放块跟列表中某块相邻,就跟该块合并。唯一的麻烦是保持指针指向对的块并调整块的大小。

/*free:将块ap插入空闲列表中 */void free(void *ap){    Header *bp, *p;    bp = (Header *)ap - 1; /* 指向块头 */    for (p = freep; !(bp > p && bp < p->ptr); p = p->s.ptr)        if (p >= p->s.ptr && (bp > p || bp < p->s.ptr))            break; /* 释放块在表开头或末尾 */    if (bp + bp->s.size == p->s.ptr) { /* 与后块合并 */        bp->s.size += p->s.ptr->s.size;        bp->s.ptr = p->s.ptr->s.ptr;    } else        bp->s.ptr = p->s.ptr;    if (p + p->s.size == bp) { /* 跟前者合并 */        p->s.size += bp->s.size;        p->s.ptr = bp->s.ptr;    } else        p->s.ptr = bp;    freep = p;}

尽管从本质上讲,存储分配程序是基于特定系统的,以上代码演示了基于特定系统部分可以是很小的一部分代码。对typedef和union的使用可以操控对齐(给定sbrk来提供合适的指针)。强制安排指针转换被精确执行,这设置解决了一个设计不合理的接口的不合理之处。尽管每个存储分配器的细节不同,但该方法可通用到其它的情形。

练习 8-6。标准库函数calloc(n, size)返回指向n个大小为size的对象的指针,并将所分配的空间初始化为0。通过调用malloc或者改写malloc来编写calloc。
练习 8-7。malloc没有检查所请求内存大小的合理性;free也认为所释放的的内存包含合理的size域。提升这两个程序使得它们能够更合理的处理这些有可能出现的错误。
练习 8-8。编写程序bfree(p, n),该函数能释放包含n字节的块p到由malloc和free维护的空闲列表中。通过使用bfree,用户可以在任何时候增加静态或全局的数组到空闲列表中。

[2016.11.01 - 10:25]

0 0
原创粉丝点击