本章例程

13.1类型无关的链表查找

#include <stdio.h>
#include "node.h"

Node *search_list(Node *node, void const *value,
                  int (*compare)(void const *, void const *))
{
    while(node != NULL){
        if(compare(&node->value, value) == 0)
            break;
        node = node->link;
    }
    return node;
}

 13.2打印命令行参数

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    while(*++argv != NULL)
        printf("%s\n",*argv);
    return 0;
}

13.3处理命令行参数

#include <stdio.h>
#define TRUE 1

void process_standard_input(void);
void process_file(char *file_name);

int option_a,option_b /*etc.*/ ;

void main(int argc, char **argv){
    while(*++argv != NULL && **argv == '-'){
        switch(*++*argv){
        case 'a':
            option_a = TRUE;
            break;
        case 'b':
            option_b = TRUE;
            break;
        /*etc.*/
        }
    }

    if(*argv == NULL)
        process_standard_input();
    else{
        do{
            process_file(*argv);
        }while(*++argv != NULL);
    }
}

13.4神秘函数

#include <stdio.h>

void mystery(int n)
{
    n += 5;
    n /= 10;
    printf("**********", 10 - n);
}

13.5把二进制值转化为字符

#include <stdio.h>

void binary_to_ascii(unsigned int value)
{
    unsigned int quotient;
    quotient = value / 10;
    if(quotient != 0)
        binary_to_ascii(quotient);
    putchar(value % 10 + '0');
}

 

本章问题

1.下面显示了一列声明。

 

从下面的列表中挑出与上面各个声明匹配的最佳描述。

 answer:

 

(建议可以试试书中所提到的cdecl程序验证,Linux的安装命令sudo apt get install cdecl)

 

2.给定下列声明:

char *array[10];
char **ptr = array;

如果变量ptr加上1,它的效果是怎样的?

answer:As with all pointer arithmetic,the value one is scaled to the size of whatever the pointer is pointing at,which in this case is a pointer to a character,the result is that ptr is advanced to point at the next element of the array.

(和所有的算术指针一样,它是可以指向任何大小的标量的指针,在这个情况下它指向一个字符,所以结果就是ptr指向数组中的下一个元素)

 

3.假定你将要编写一个函数,它的起始部分如下所示:

void func(int ***arg){

参数类型是什么?画一张图,显示这个变量的正确用法,如果想取得这个参数所指代的整数,你应该使用怎样的表达式?

answer:

(这是一个指向指针的指针的指针,表达式***arg可以获得这个参数指向的整数)

 

4.下面的代码可以如果进行改进?

Transaction *trans;
trans->product->orders += 1;
trans->product->quantity_on_hand -= trans->quantity;
trans->product->supplier->reorder_quantity += trans->quantity;
if(trans->product->export_restricted){
    ...
}

answer:

把trans声明为寄存器变量可能有所帮助,这取决于你使用的环境。在有些机器上,把指针放入寄存器的好处相当突出,其次,声明一个保存trans->product值的局部变量。这个表达式可以被多次使用,但不需要每次重新计算,有些编译器会自动为你做这两件事,但有些编译器不会。

这个Transaction的数据结构可能是下面这个样子的:

 而不像链表中没有其他的数据结构,所以可以用Node **,而不用Transaction **.

 

5.给定下列声明:

typedef struct{
    int x;
    int y;
}Point;

Point *p;
Point *a = &p;
Point **b = &a;

判断下面各个表达式的值:

answer:

a = &p

b = p

c = p.x

d = &a

e illegal

f  illegal

g = a = &p

h illegal

i illegal

j illegal

k illegal

l  = x

m = p

 

6.给定下列声明:

typedef struct{
    int x;
    int y;
}Point;

Point x,y;
Point *a = &x, *b = &y;

解释下列各语句的含义。

a. x = y;

b. a = y;

c. a = b;

d. a = *b;

e. *a = *b;

answer:

a. Copie the values from y into x

(将y的值赋给x)

b. Illegal,as a is a pointer but y is not

(非法,因为a是一个指针而y不是)

c. Copie the pointer value from b into a

(复制指针b的值给a)

d. Illegal,as a is a pointer but *b is not

(非法,a是一个指针而*b不是)

e. Copies the value from y into x

(把y的值赋给x)

 

7.许多ANSI C的实现都包含了一个函数,称为getopt。这个函数用于帮助处理命令行参数,但是,getopt在标准中并未提及,拥有这样一个函数,有什么优点?又有什么缺点?

answer:

它的唯一优点如此明显,你可能没有对它多加思考,这也是编写这个函数的理由--这个函数使处理命令行参数更为容易,但这个函数的其他方面都是不利因素,你只能使用这个函数所支持的方式处理参数。由于它并不是标准的一部分,所以使用getopt将会降低程序的可移植性。

 

 8.下面的代码段有什么错误(如果有的话)?你如何修正它?

char *pathname = "/usr/temp/xxxxxxxxxxxxxxx"
...
/*
** insert the filename in to the pathname;
*/
strcpy(pathname + 10, "abcde");

 answer:If the filename exceeds(超过) 15 characters(this one does not),it will overflow the memory allocated to hold the string literal,If the initial value of the pathname is ever changed,you must remember to change the literal constant ten also,but the real problem is that it overwrites the string literal,the effects of which are not defined by the Standard,The solusion is to declare pathname as an array,not a pointer.

(如果文件名超过15个字符,为了保存这个字符串会使内存溢出,如果pathname的初始值被改过,你必须记得改变字面值常量10,不过在实际问题中,字符串经常被更改,其影响是标准未定义的,这个解决办法应该声明pathname为一个数组,而不是一个指针)

 

9.下面的代码段有什么错误(如果有的话)?你如何修正它?

char pathname[] = "/usr/temp/";
...
/*
** append the filename in to the pathname;
*/
strcat(pathname, "abcde");

answer:The array is only as large as needed to hold its initial value,so appending anything to it overflows the memory array,probably overwriting some other variables,The solution is to make the array enough larger than the initail value to hold the longest filename that will be appended to it later,A potential(可能的) problem is the use of strcat;if this pathname prefix(前缀) is suppoised to be used with several different filenames,appending the next one with strcat will not produce the desired result.

(这个数组的大小只能容纳初始值,所以在后面添加任何东西都会使数组内存溢出,可能会覆盖其他变量,这个解决办法要使数组足够大可以包含最长的文件名,一个可能存在问题的地方是strcat,如果这个文件名前缀允许使用若干不同的文件名,在后面连接一个字符串可能不会产生预期的效果)

 

10.下面的代码段有什么错误(如果有的话)?你如何修正它?

char pathname[20] = "/usr/temp/";
...
/*
** append the filename in to the pathname;
*/
strcat(pathname, filename);

answer:If the string in filename is longer than nine characters,it will overflow the array,if the length of a pathname has an upper bound,the array should be declared at least that large.Otherwise,the length of the filename can be computed and used to obtain memeory dynamically.

(如果filename中的字符长度比九还长,将会溢出数组,如果pathname的长度有一个上界,数组至少需要声明这个长度,否则,如果filename的长度能被计算可以使用动态内存获取)

 

11.标准表示如果对一个字符串常量进行修改,其效果是未定义的,如果你修改了字符串常量,有可能会出现什么问题呢?

answer:首先,有些编译器把字符串常量存放在无法进行修改的内存区域,如果你试图对这类字符串常量进行修改,就会导致程序终止,其次,即使一个字符串常量在程序中使用的地方不止一处,有些编译器只保存这个字符串常量的一份拷贝。修改其中一个字符串常量将影响程序中这个字符串常量出现的所有地方,这使得调试工作极为困难,例如,如果一开始执行了下面这条语句

strcpy("hello\n", "Bye!\n");

然后再执行下面这条语句:

printf("hello\n");

将打印出Bye!

 

本章练习

1.编写一个程序,从标准输入中读取一些字符,并根据下面的分类计算各类字符所占的百分比:

控制字符

空白字符

数字

小写字母

大写字母

标点符号

不可打印字符

这些分类是根据ctype.h中的函数定义的,不能使用一系列的if语句。

answer:

#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>

int unprint(int ch){
    return !isprint(ch);
}

int (*test[])(int ch) = {
    iscntrl,
    isspace,
    isdigit,
    islower,
    isupper,
    ispunct,
    unprint
};

#define COUNT (sizeof(test) / sizeof(test[0]))

char *label[] = {
    "control",
    "whitespace",
    "digit",
    "lower case",
    "upper case",
    "punctuation",
    "unprintable"
};

int value[COUNT];
int total;

int main()
{
    int i;
    int ch;
    while((ch = getchar()) != EOF){
        total += 1;
        for(i = 0; i < COUNT; i++)
            if(test[i](ch))
                value[i]++;
    }

    if(total == 0)
        printf("no character in the input\n");
    else{
        for(i = 0; i < COUNT; i++){
            printf("%3.0f%% %s characters\n",
                   value[i] * 100.0 / total,
                   label[i]);
        }
    }

    return 0;
}

 

2.编写一个通用目的的函数,遍历一个单链表,它应该接受两个参数,一个指向链表第一个节点的指针,和一个指向回调函数的指针,回调函数应该接受单个参数,也就是指向一个链表节点的指针,对于链表中的每个节点,都应该调用一次这个回调函数。这个函数需要知道链表节点的什么信息?

 answer:

This function is probably not terribly useful,as it is nearly as easy to traverse the list on your own as it is to call a general purpose function to do it,On the other hand,the paradigm is valuable enough to justify this exercise.Note that the function still needs to know where the link is in the node structure,so it isn't all that general after all.

(这个函数可能不是特别有用,因为你自己调用一个通用目的函数来遍历这个链表几乎一样容易,另一方面,这个练习足以证明这个范式是有用的,注意这个函数仍然需要知道链表指向的节点的结构,所以它并不完全通用)

#include <stdio.h>
#inclue "node.h"

void sll_traversal(Node *root,void (*trav)(Node *))
{
    while(root != NULL){
        trav(root);
        root = root->link;
    }
}

 

3.转换下列代码段,使它改用转移表而不是switch语句。

Node *list;
Node *current;
Transaction *transaction;
typedef enum {NEW, DELETE, FORWARD, BACKWORD,
              SEARCH,EDIT} Trans_type;
...
switch(transaction->type){
case NEW:
    add_new_trans(list,transaction);
    break;
case DELETE:
    current = delete_trans(list,transaction);
    break;
case FORWORD:
    current = current->next;
    break;
case BACKWORD:
    current = current->prev;
case SEARCH:
    current = search(list,transaction);
    break;
case EDIT:
    edit(current,transaction);
    break;
default:
    printf("Illegal transaction type!\n");
    break;
}              

answer:

 Several of the cases do their work directly;this code must be moved into functions,The main difficulty here is to comp up with a common prototype that will serve the needs of all of the necessary functions,it must contain each argument used by any function,the new functions must use the common prototype,and the existing function must be modified to match it,Most of the function will end up with at least one argument that they do not need,A pointer must be passed to current so that the appropriate funcitons can modify it.

(在不同的情况下执行不同的工作,这些代码必须移到函数中,主要的困难在于提出一个共同的原型来适应不同需要的函数,要包括任何一个函数使用的所有参数,新的函数都要使用这个共同的原型,已经存在的函数要转换来匹配新的原型,大多数函数至少有一个它们不需要的参数结尾,current必须通过指针来使函数修改它) 

void add_new_trans(Node *list, Node **current, Transaction *trans);
void delete_trans(Node *list, Node **current, Transaction *trans);
void search(Node *list, Node **current, Transaction *trans);
void edit(Node *list, Node **current, Transaction *trans);

void forword(Node *list, Node **current, Transaction *tran)
{
    *current = (*current)->next;
}

void backword(Node *list, Node **current, Transaction *tran)
{
    *current = (*current)->prev;
}

void (*function[])(Node *, Node **, Transaction *) = {
    add_new_trans,
    delete_trans,
    forword,
    backword,
    search,
    edit
};

#define COUNT (sizeof(function) / sizeof(function[0]));

if(transaction->type >= NEW && transaction <= EDIT)
    function[transaction->type](list, &current, transaction);
else
    printf("Illegal transaction type!\n");

 

4.编写一个名叫sort的函数,它用于对一个任何类型的数组进行排序,为了使函数更为通用,他的其中一个参数必须是一个指向比较回调函数的指针,该回调函数由调用程序提供,比较函数接受两个参数,也就是两个指向需要进行比较的值的指针,如果两个值相等,返回0,如果第一个值小于第二个返回小于零的整数,否则,返回大于零的整数。sort函数的参数将是:

1.一个指向需要排序的数组的第一个值的指针

2.数组中值的个数

3.每个数组元素的长度

4.一个指向比较回调函数的指针

sort函数没有返回值。你将不能根据实际类型声明数组参数,因为函数应该可以对不同类型的数组进行排序,如果你把数据当成一个字符数组使用,你可以用第三个参数寻找实际数组中每个元素的起始位置,也可以用它交换两个数组元素(每次一个字节)。

对于简单的交换排序,你可以使用下面的算法,当然也可以使用你认为更好的算法。

 answer:This function is patterned after the qsort function provided in the ANSI C library,The only two tricks are locating the beginning of array element and interchanging two elements.The element length is used for both tasks.

(这个函数是模仿ANSI C库中提供的qsort函数,其中有两个小把戏,一个是传递数组首元素的位置,还有就是交换两个元素,两个函数中都使用了元素的长度)

 

5.编写代码处理命令行参数是十分乏味的,所以最好有一个标准函数来完成这项工作。但是不同的程序以不同的方式处理它们的参数,所以,这个函数必须非常灵活,以便使它能用于更多的程序。在本题中,你将编写这样一个函数。你的函数通过寻找和提取参数来提供灵活性。用户所提供的回调函数将执行实际的处理工作。下面是函数的原型。注意它的第四个参数和第五个参数是回调函数的原型。

char ** do_args(int argc, char **argv, char *control,
        void (*do_arg)(int ch, char *value),
         void (*illegal_arg)(int ch));

头两个参数就是main函数的参数,main函数对它们不做修改,直接传递给do_args,第三个参数是个字符串,用于标志程序期望接受的命令行参数。最后两个参数都是函数指针,它们是由用户提供的。do_args函数按照下面这样的方式处理命令行参数:

跳过程序名参数

while 下一次参数以一个横杆开头

  对于参数横杠后面的每个字符

    处理字符

返回一个指针,指向下一个参数指针

为了“处理字符”,你首先必须观察该字符是否位于control字符串内,如果它并不位于那里,调用illegal_arg指向函数,把这个字符作为参数传递过去。如果它位于control字符串内,但它的后面并不是跟一个+号,那么就调用do_arg所指向的函数,把这个字符和一个NULL指针作为参数传递过去。

如果该字符位于control字符串内并跟一个+号,那么就应该有一个值与这个字符相联系。如果当前参数还有其他字符,它们就是我们需要的值。否则,下一个参数才是这个值。在任何一种情况下,你应该调用do_arg所指向的函数,把这个字符和指向这个值的指针传递过去。如果不存在这个值(当前参数没有其他字符,且后面不再有参数),那么你应该调用Illegal_arg函数。注意:你必须保证这个值中的字符以后不会被处理。

当所有以一个横杆开头的参数被处理完毕后,你应该返回一个指向下一个命令行参数的指针的指针(也就是一个诸如&argv[4]或argv+4的值)。如果所有的命令行参数都以一个横杆开头,你就返回一个指向“命令行参数列表中结尾的NULL指针”的指针。

这个函数必须不能修改命令行参数指针,也不能修改参数本身,为了说明这一点,假定程序prog调用这个函数:下面的例子显示了几个不同集合的参数的执行结果。

answer:

#include <stdio.h>
#include <stdlib.h>

enum {NONE, FLAG, ARG};

argtype(register int ch, register int *control)
{
    while(*control != '\0')
        if(ch == *control++)
            return *control == '+' ? ARG : FLAG;
    return NONE;
}

char **do_args(int argc, char **argv, char *control,
               void (*do_arg)(int ch, char *value),
               void (*illegal_arg)(int ch))
{

    register char *argp;
    register int ch;
    register int skip_arg;

    while((argp = *++argv) != NULL && *argp == '-'){
        skip_arg = 0;
        while(!skip_arg && (ch = *++argp) != '\0'){
            switch(argtype(ch, control)){
            case FLAG:
                (*do_arg) (ch, NULL);
                break;
            case ARG:
                if(*++argp != '\0' || (argp = *++argv) != NULL){
                    (*do_arg)(ch, argp);
                    skip_arg = 1;
                    break;
                }
                (*illegal_arg)(ch);
                return argv;
            case NONE:
                (*illegal_arg)(ch);
                break;
            }
        }
    }
    return argv;
}

 

这可能是目前为止在这本书中学习最难的章节了,尤其是之前没有接触过回调函数、转接表和命令行参数,学习这一章可能会异常艰辛,所以要加油挺过这一关,可以多看几遍书,题目的话也要不停的阅读弄清楚意思了再动手,读不懂题目可能还是对内容不熟悉,另外这一章里的介绍的命令行参数的处理过程还是非常有用的,尤其对于喜欢用命令行的人和在Linux/Unix下工作的人,算是比较底层的东西。