读《C程序设计语言》2E-3 [1~3]章

2016.01.07 - 01.27

述标题为3时有点犹豫不好意思,因为该书的前两次读似超不成熟(这次做一下各章的练习题):

  • 读《TCPL》I
  • 读《TCPL》II

平台
i7-4790 + windows 10 x64
VMware® Workstation 12 Pro + ubuntu-12.04.5-desktop-i386(Linux ubuntu 3.13.0-32-generic) + gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)

01.08

1 导言

练习 1-1

在你自己的系统中运行”hello, world”程序。再有意去掉程序中的部分内容,看看会得到什么出错信息。

在自己的系统中运行”hello, world”程序,首先必须编写程序文本(vim),然后成功地对该文件进行编译(gcc),再加载(shell)、运行程序。
p11.c

#include <stdio.h>

main()
{
    printf("hello, world\n");
}

在shell程序中使用gcc编译(、连接)p11.c程序得可执行文件p11,再运行p11:
gcc p11.c -o pll [编译]
./pll [加载、运行]
hello, world [结果]

当有意去掉p11.c程序中的部分内容时,在编译p11.c的过程中会出错且得不到可执行文件p11。每种程序语言都有其特定的编程格式,编译器是根据某个的操作系统和某种编程语言而被编写的程序。若用某种编程语言进行编程但不按其格式编写程序,那么用它的编译器检查该程序时编译器就会报错,因为该编译器只能够根据该程序语言规定的编程格式对程序(中的语句)进行翻译。C语言程序会被C编译器翻译成汇编程序。

练习1-2

做个试验,当printf函数的参数字符串中包含\c(其中c是上面的转义字符序列中未曾列出的一个字符)时,观察一下会出现什么情况。

书中列出了’\n’, ‘\t’, ‘\b’, ‘\”’, ‘\’转义字符序列。那么可以在printf函数的参数字符串中就包含\c,观察现象。
p12.c

#include <stdio.h>

main()
{
    printf("hello, \c world\n");
}

在shell程序中使用gcc编译(、连接)p12.c程序得可执行文件p12,再运行p12:
gcc p12.c -o p12 [编译]
p12.c: In function ‘main’:
p12.c:5:9: warning: unknown escape sequence: ‘\c’ [enabled by default]
./p12 [加载、运行]
hello, c world [结果]

转义字符序列为表示无法输入的字符或不可见字符提供了一种输入方式。在遇到转义字符如‘\n’时,程序执行换行操作(将显存地址变为屏幕下一行开始处对应的地址)。当用\符号与其它字符(不能与其组成成转义字符的字符)组合时,编译器不能识别。gcc给出警告信息并处理’\c’为’c’。

练习 1-3

修改温度转换程序,使之能在转换表的顶部打印一个标题。

保留书中P.7中的程序,在while循环前加标题:
p13.c

#include <stdio.h>

/* 当fahr=0, 20, ..., 300时,打印华氏温度与摄氏温度对照表;*/
main()
{
    float fahr, celsius;
    int lower, upper, step;

    lower   = 0;
    upper   = 300;
    step    = 20;

    printf("华氏温度与摄氏温度对照表(0 ~ 300):\n");
    fahr    = lower;
    while (fahr <= upper) {
        celsius = (5.0 / 9.0) * (fahr - 32.0);
        printf("%3.0f %6.1f\n", fahr, celsius);
        fahr    = fahr + step;
    }
}

在shell程序中使用gcc编译(、连接)p13.c程序得可执行文件p13,再运行p13:
gcc p13.c -o p13 [编译]
./p13 [加载、运行]

[结果]

用’/’于整数时,只能得到整数的商。欲得更精确的对照表,就得采取浮点数除法。

一个C语言程序,无论其大小如何,都是由函数和变量组成的。函数中包含一些语句,以指定所要执行的计算操作;变量则用于存储计算过程中使用的值。通常情况下,函数名没有特殊的限制,但main时一个特殊的函数名 - 每个程序都从main函数开始执行,这意味着每一个C语言程序都必须在某个文件中包含一个main函数。

C语言中的变量必须先声明后使用。声明用于说明变量的属性,它由一个类型名和一个变量表组成。C语言中的数据类型对象(变量)的大小取决于具体的机器(计算机)。

练习 1-4

编写一个程序打印摄氏温度转换为华氏温度的转换表。

采用“练习 1-3”中的程序模板,由celsius = (5.0 / 9.0) * (fahr - 32.0)得fahr = celsius * (9.0 / 5.0) + 32.0。在程序中以celsius为已知值,再根据celsius和fahr的转换关系便可得到摄氏温度和华氏温度的转换表。
p14.c

#include <stdio.h>

/* 当celsius=-17.8, -6.7, ..., 148.9时,打印摄氏温度与华氏温度对照表*/
main()
{
    float celsius, fahr;
    float lower, upper, step;

    lower   = -17.8;
    upper   = 148.9;
    step    = 11.1;

    printf("摄氏温度与华氏温度对照表(-17.8 ~ 148.9):\n");
    celsius = lower;
    while (celsius <= upper) {
        fahr    = celsius * (9.0 / 5.0) + 32.0;
        printf("%5.1f %3.0f\n", celsius, fahr);
        celsius = celsius + step;
    }
}

若基数为浮点数值时,lower/upper/step都要相应为浮点类型变量。

在shell程序中使用gcc编译(、连接)p14.c程序得可执行文件p14,再运行p14:
gcc p14.c -o p14 [编译]
./p14 [加载、运行]
读《C程序设计语言》2E-3 [1~3]章_第1张图片
[结果]

01.09

练习 1-5

修改温度转换程序,要求以逆序(即按照从3000度到0度的顺序)打印温度转换表。

(1) 表达式和for

在“练习1-3”的基础上减少程序变量,并用for循环实现:

#include <stdio.h>

/* 当fahr=300, 280, ..., 0时,打印华氏温度和摄氏温度对照表 */
main()
{
    int fahr;

    printf("华氏温度和摄氏温度对照表:(300 ~ 0)\n");
    for (fahr = 300; fahr >= 0; fahr = fahr - 20) {
        printf("%3d %5.1f\n", fahr, (5.0 / 9.0) * (fahr - 32.0));
    }
}

[1] C语言中一个通用规则 - 允许使用某种变量值的任何场合,都可以使用该类型的更复杂的表达式(如程序中的(5.0 / 9.0) * (fahr - 32.0))。
[2] 在具有整型和浮点数的表达式中,整型会隐式的转换为浮点型后再参与表达式的计算。

这样程序的可读性似有降低,但程序量减少。

(2) 符号常量

在以上程序中的300、20等类似的常数几乎无法向阅读该程序的人提供什么信息,而且使程序的修改变得更加的困难(若在一个大型程序中同一个常数出现多次)。处理这种常数的一种方法是赋予它们有意义的名字,#define指令可以把(符号)常量定义为一个特定的字符串:

#include <stdio.h>

#define LOWER 0 /* 表的下限 */
#define UPPER 300 /* 表的上限 */
#define STEP 20 /* 步长 */

/* 当fahr=300, 280, ..., 0时,打印华氏温度和摄氏温度对照表 */
main()
{
    int fahr;

    printf("华氏温度和摄氏温度对照表:(300 ~ 0)\n");
    for (fahr = UPPER; fahr >= LOWER; fahr = fahr - STEP) {
        printf("%3d %5.1f\n", fahr, (5.0 / 9.0) * (fahr - 32.0));
    }
}

符号常量名通常用大写字母拼写,这样就容易与用小写字母拼写的变量名相区别。

在shell程序中使用gcc编译(、连接)p15.c程序得可执行文件p15,再运行p15:
gcc p15.c -o p15 [编译]
./p15 [加载、运行]
读《C程序设计语言》2E-3 [1~3]章_第2张图片
[结果]

练习 1-6

验证表达式getchar() != EOF的值是0还是1。

#include <stdio.h>

/* 验证表达式getchar() != EOF的值 */
main()
{
    printf("%d\n", getchar() != EOF);
}

在shell程序中查看getchar()函数功能 - man getchar:
int getchar(void);

  • 功能/返回值 - 读取标准输入流(stdin)中的字符,将该字符当作unsigned char类型并将其转换为 int返回,若遇到错误或文件结束则返回EOF。

在shell程序中使用gcc编译(、连接)p16.c程序得可执行文件p16,再运行p16:
gcc p16.c -o p16 [编译]
./p16 [加载、运行]因为getchar读取标准输入流中的字符,所以程序运行后在键盘中敲入一个字符到标准输入流中让getchar读取 - 当用键盘敲入”Ctrl + d/D”时程序输出0;程序响应其它键盘输入(用回车表示输入的结束)都输出1。

练习 1-7

编写一个打印EOF值的程序。

EOF(end of file)是与任何实际字符都不同的特殊值。EOF被定义在头文件stdio.h中,是一个整型值。那么,在程序中就可以用printf + %d打印EOF值。

#include <stdio.h>

/* 打印EOF的值 */
main()
{
    printf("%d\n", EOF);
}

在shell程序中使用gcc编译(、连接)p17.c程序得可执行文件p17,再运行p17:
gcc p17.c -o p17 [编译]
./p17 [加载、运行]
-1 [结果]

EOF定义在stdio.h中,在该平台下的stdio.h中被定义为-1。

练习 1-8

编写一个统计空格、字符表与换行符个数的程序。

以键盘输入(标准输入流)作为程序的输入,用getchar()函数从标准输入流中读取键盘输入并统计空格、制表符与换行符的个数。

/* p18.c
 * 统计键盘输入字符串中的空格、制表符与换行符的个数
 */
#include <stdio.h>

main()
{
    int c;
    long int sn, tn, en;

    c = sn = tn = en = 0;

    while ((c = getchar()) != EOF) {
        if (c == ' ') {
            sn++;
        }else if (c == '\t') {
            tn++;
        }else if (c == '\n') {
            en++;
        }else {
            ;
        }
    }

    printf("空格数\t制表符\t换行符\n%ld\t%ld\t%ld\n", sn, tn, en);
}

当输入EOF(Linux下为Ctrl + d/D)时表结束输入,程序统计结束并输出统计结果。

在shell界面中用gcc编译(连接)p18.c得p18,运行p18,运行结果如下:

练习 1-9

编写一个将输入复制到输出的程序,并将其中连续的多个空格用一个空格代替。

在程序中设置一个变量用于记录上一个字符是否为空格,若上一个字符为空格且当前字符亦为空格则不输出并保持变量标记上一个为空格的值;若当前字符不为空格则清除标记上一个字符为空格的值并输出当前字符。

/* p19.c * 将输入复制到输出,将其中连续的多个空格用一个空格代替 */
#include <stdio.h>

main()
{
    char f;
    int c;

    f = c = 0;

    while ((c = getchar()) != EOF) {
        if (c == ' ') {

            if (f != ' ') {
                f   = ' ';
                putchar(c);
            }else if (f == ' ') {
                ;
            }
        }else {
            putchar(c);
            f   = 0;
        }
    }
}

在shell界面中用gcc编译(连接)p19.c得p19,运行p19(每当输入一个回车或Ctrl + d/D时,getchar才会读输入内容。欲在输入EOF后才将输入输出到标准输出,可将处理过后的字符串存储到某缓冲区中),运行结果如下:
这里写图片描述

练习 1-10

编写一个将输入复制到输出的程序,并将其中的制表符替换为\t,把回退符替换为\b,把反斜扛替换为\。这样可以将制表符和回退符以可见的方式显示出来。

/* p110.c * 将输入复制到输出,用转义字符形式显示制表符、回退符 */
#include <stdio.h>

main()
{
    int c = 0;

    while ((c = getchar()) != EOF) {
        if (c == '\t') {
            printf("\\t");
        }else if (c == '\b') {
            printf("\\b");
        }else if (c == '\\') {
            printf("\\\\");
        }else {
            putchar(c);
        }
    }
}

因为C语言库中的I/O函数为终端流默认设置为行缓冲,当检测到回车或EOF的输入时才将输入流中的一串输入读入缓冲区中再慢慢进行处理。所以在输入流中的回退键(退格键 - Backspace)根本进入不了C语言I/O库函数设置的缓冲区中,故而没法显示回退键。欲显示回退键,想一下其它办法。

没有修改程序,问了下度娘linux下的回退键的组合键为Ctrl + h,所以基本能够验证该程序了。

在shell界面中用gcc编译(连接)p110.c得p110,运行p110:

图中^H^H就是用Crtl + h组合键完成的回退输入(用int 9键盘输入中断处理程序中断或许能够接收真实的回退输入,见 读《汇编语言》3E-III[摘 检测点 实验 课程设计] - 实验 15 安装新的int 9 中断例程)。

01.11

练习 1-11

你准备如何测试单词计数程序?如果程序中存在某种错误,那么什么样的输入可能发现这类错误呢?
P.14中的单词计数程序如下:

#include <stdio.h>

#define IN 1 /* 在单词内 */
#define OUT 0 /* 在单词外 */

/* 统计输入的行数、单词数与字符数 */
main()
{
    int c, nl, nw, nc, state;

    state   = OUT;
    nl = nw = nc = 0;
    while ((c = getchar()) != EOF) {
        ++nc;
        if (c == '\n')
            ++nl;
        if (c == ' ' || c == '\n' || c == '\t')
            state   = OUT;
        else if (state == OUT) {
            state   = IN;
            ++nw;
        }
    }
    printf("%d %d %d\n", nl, nw, nc);
}

该程序统计单词的方式为“遇到单词计数” - 若当前字符不为空格、换行符以及制表符且在单词外(state = OUT)则统计一个单词;若当前字符为空格或换行符或制表符时则置state = OUT。这种方法没有问题,但仅判断空格、换行符以及制表符是不够的 - 不能把诸如Ctrl + F、逗号(,)等这样的输入当成单词。

在shell界面下用gcc编译以上程序并运行,然后用以下输入测试程序可以发现程序中的一个错误:
这里写图片描述
连续输入的两个Ctrl + F键被程序当成了单词。

练习 1-12

编写一个程序,以每行一个单词的形式打印其输入。
练习 1-11的基础之上编写该程序:

  • 不能仅靠空格、换行以及制表符来判断单词(逗号等符号,命令行中其它的一些不可见的输入)。
  • 遇到单词时(可组成单词的字符)打印单词至单词结束(遇非组成单词的字符)。

编写程序如下:

/* p112.c
 * 以每行一个单词的形式打印输入
 */
#include <stdio.h>

#define IN 1 /* 在单词内 */
#define OUT 0 /* 在单词外 */


main()
{
    int c, nl, nw, nc, state, in_bf;

    state = in_bf = OUT;
    nl = nw = nc = 0;
    while ((c = getchar()) != EOF) {
        ++nc;
        if (c == '\n')
            ++nl;
        if (c == ' ' || c == '\n' || c == '\t') {/* 这里判断的应该是凡是不能组成单词的可能输入的字符 */
            state   = OUT;

            if (in_bf == IN) {
                in_bf   = OUT;
                putchar('\n');
            }
        } else if (state == OUT) {
            state   = IN;
            in_bf   = IN;
            putchar(c);
            ++nw;
        } else if (state == IN) {
            putchar(c);
        }
    }
    printf("行\t单词数\t字符数\n%d\t%d\t%d\n", nl, nw, nc);
}

在shell界面中用gcc编译(连接)p112.c得p112,运行p112:
读《C程序设计语言》2E-3 [1~3]章_第3张图片

01.12

练习 1-13

编写一个程序,打印输入中单词长度的直方图。水平方向的直方图比较容易绘制,垂直放图则要苦难些。

(1) 打算

简单的打算如下:
读《C程序设计语言》2E-3 [1~3]章_第4张图片

(2) 实现打算

[1] 获取每个单词长度于数组中

/* p113.c
 * 打印输入中(一行)单词长度的直方图 - 垂直方向
 */
#include <stdio.h>

#define IN 1 /* 在单词内 */
#define OUT 0 /* 在单词外 */
#define WORD_NUM 100 /* 单词最大数量 */

main()
{
    int c, nw, state;
    unsigned int i, wl[WORD_NUM];

    nw      = 0;
    state   = OUT;
    for (i = 0; i < WORD_NUM; ++i)
        wl[i]   = 0;

    i   = 0;
    while ((c = getchar()) != EOF) {
        if (c == ' ' || c == '\n' || c == '\t') { /* 这里判断的应该是凡是不能组成单词的可能输入的字符 */
            state   = OUT;
        } else if (state == OUT) {
            state   = IN;
            ++nw;
            ++wl[nw - 1];
        } else if (state == IN) {
            ++wl[nw - 1];
        }
    }
    printf("单词长度\n");
    for (i = 0; i < nw; ++i)
        printf("%u\n", wl[i]);
}

在shell界面下用gcc编译、连接p113.c,并运行进行简单的测试:
读《C程序设计语言》2E-3 [1~3]章_第5张图片

[2] 找单词长度最大值 - 线性查找法
对于这种小输入,可用线性查找法查找单词长度最大值(线性查找已具有良好的性能);若程序性能受瓶颈干扰,则可考虑用折半查找发代替线性查找法。

/* p113.c
 * 打印输入中(一行)单词长度的直方图 - 垂直方向
 */
#include <stdio.h>
...

main()
{
    unsigned int i, mwl, wl[WORD_NUM];

    mwl    = 0;
    ......
    for (i = 0; i < nw; ++i) {
        if (wl[i] > mwl)
            mwl = wl[i];
    }
    printf("单词长度最大值:%u\n", mwl);
}

在shell下简单测试该线性查找法:
读《C程序设计语言》2E-3 [1~3]章_第6张图片

[3] 打印图案

/* p113.c * 打印输入中(一行)单词长度的直方图 - 垂直方向 */
#include <stdio.h>

......

main()
{
    unsigned int i, j, mwl, wl[WORD_NUM];

    ......

    /* 打印图案 */
    printf("打印单词长度直方图(星星图):\n");
    for (j = 0; j < mwl; ++j) {
        for (i = 0; i < nw; ++i) {
            if (mwl - j > wl[i]) {
                printf(" \t");
            } else {
                printf("*\t");
            }
        }
        printf("\n");
    }
    printf("\n");
}

在shell下简单调试p113.c程序:

若将星星换成直方图单元再加上坐标则可打印出单词长度的直方图。

练习 1-14

编写一个程序,打印输入中各个字符出现频度的直方图。

(1) 打算

简单的打算如下:
读《C程序设计语言》2E-3 [1~3]章_第7张图片

(2) 实现打算

[1] 统计字符个数

/* p114.c
 * 打印各个字符出现频度的直方图 - 垂直方向
 */
#include <stdio.h>

#define IN 1 /* 在单词内 */
#define OUT 0 /* 在单词外 */
#define CHAR_NUM 127 /* 字符个数 */
#define NOT_CHAR EOF /* 非字符 */

main()
{
    int c, state;
    unsigned int i, nc, ch[CHAR_NUM], ch_fre[CHAR_NUM];

    nc      = 0;
    state   = OUT;
    for (i = 0; i < CHAR_NUM; ++i) {
        ch[i]       = NOT_CHAR;
        ch_fre[i]   = 0;
    }

    while ((c = getchar()) != EOF) {
        ++nc;
        ch[c]   = c;
        ++ch_fre[c];
    }
    printf("字符串总数:%u\n各个字符情况:\n", nc);
    for (i = 0; i < CHAR_NUM; ++i) {
        if (ch_fre[i] != 0)
            printf("%c - %u\n", ch[i], ch_fre[i]);
    }
}

在shell界面下简单的测试p114.c:
读《C程序设计语言》2E-3 [1~3]章_第8张图片
简单测试统计字符个数的程序

[2] 计算字符频度

/* p114.c
 * 打印各个字符出现频度的直方图 - 垂直方向
 */
#include <stdio.h>

#define IN 1 /* 在单词内 */
#define OUT 0 /* 在单词外 */
#define CHAR_NUM 127 /* 字符个数 */
#define NOT_CHAR EOF /* 非字符 */

main()
{
    int c, state;
    unsigned int i, ch[CHAR_NUM];
    float nc, ch_fre[CHAR_NUM];

    nc      = 0;
    state   = OUT;
    for (i = 0; i < CHAR_NUM; ++i) {
        ch[i]       = NOT_CHAR;
        ch_fre[i]   = 0;
    }

    while ((c = getchar()) != EOF) {
        ++nc;
        ch[c]   = c;
        ++ch_fre[c];
    }
    printf("字符串总数:%.0f\n各个字符情况:\n", nc);
    for (i = 0; i < CHAR_NUM; ++i) {
        if (ch_fre[i] != 0)
            printf("%c - %.0f\n", ch[i], ch_fre[i]);
    }


    printf("各字符的频度:\n");
    for (i = 0; i < CHAR_NUM; ++i) {
        if (ch_fre[i] != 0) {
            ch_fre[i]   = ch_fre[i] / nc;
            printf("%c - %.2f\n", ch[i], ch_fre[i]);
        }
    }
}

在shell界面下简单的测试该程序:
读《C程序设计语言》2E-3 [1~3]章_第9张图片
频度调试结果

[3] 打印图案
首先找到最大频度值,然后选择频度的基本单元(一个*代表多大频度 - 这里保留了两位小数,故而可将一个星号对应0.01)。

/* p114.c * 打印各个字符出现频度的直方图 - 垂直方向 */
#include <stdio.h>

#define IN 1 /* 在单词内 */
#define OUT 0 /* 在单词外 */
#define CHAR_NUM 127 /* 字符个数 */
#define NOT_CHAR EOF /* 非字符 */
#define ACCURACY 0.01 /* 频度精度 */

main()
{
    int c, state;
    unsigned int i, r, ch[CHAR_NUM];
    float j, nc, mfre, ch_fre[CHAR_NUM];

    r       = 0;
    nc      = 0;
    state   = OUT;
    mfre    = 0;
    for (i = 0; i < CHAR_NUM; ++i) {
        ch[i]       = NOT_CHAR;
        ch_fre[i]   = 0;
    }

    /* 统计输入的字符数 */
    while ((c = getchar()) != EOF) {
        ++nc;
        if (c == '\n') ch[c] = 'H';
        else if (c == '\t') ch[c] = 'T';
        else ch[c]  = c;
        ++ch_fre[c];
    }
    printf("字符串总数:%.0f\n各个字符情况:\n", nc);
    for (i = 0; i < CHAR_NUM; ++i) {
        if (ch_fre[i] != 0)
            printf("%c - %.0f\n", ch[i], ch_fre[i]);
    }


    /* 计算各字符的频度 */
    printf("各字符的频度:\n");
    for (i = 0; i < CHAR_NUM; ++i) {
        if (ch_fre[i] != 0) {
            ch_fre[i]   = ch_fre[i] / nc;
            printf("%c - %.2f\n", ch[i], ch_fre[i]);
        }
    }


    /* 线性查找最大频度 */
    for (i = 0; i < CHAR_NUM; ++i) {
        if (mfre < ch_fre[i])
            mfre    = ch_fre[i];
    }
    printf("最大频度值:%.2f\n", mfre);

    /* 打印直方图图案 */
    for (j = 0; j < mfre; j += ACCURACY) {
        for (i = 0; i < CHAR_NUM; ++i) {
            if (ch_fre[i] >= ACCURACY) {
                if (mfre - r * ACCURACY > ch_fre[i]) {
                    printf(" \t\t");
                }else {
                    printf("*\t\t");
                }
            }
        }
        ++r;
        printf("\n");
    }
    for (i = 0; i < CHAR_NUM; ++i) {
        if (ch_fre[i] >= ACCURACY)
            printf("%c=%.2f%%\t\t", ch[i], ch_fre[i]);
    }
    printf("\n");
}

程序中将回车和tab键分别用’H’和’T’表示。

在shell界面下简单的对改程序进行测试:
读《C程序设计语言》2E-3 [1~3]章_第10张图片
统计输入字符的直方图

跟浮点数相关的计算涉及到关于精度的问题。

01.13

练习 1-15

重新编写(1.2节)中的温度转换程序,使用函数实现温度转换计算。
选择“练习1-5”中的代码作为本次练习基础。

(1) 子函数声明/定义

定义是声明的一种特殊形式。

/* p1_15.c
 * 使用子函数 - 基于p1_5.c程序
 */
#include <stdio.h>

#define LOWER 0 /* 表上限 */
#define UPPER 300 /* 表下限 */
#define STEP 20 /* 步长 */

/* 当华氏温度为300, 280, ..., 0时,打印华氏温度和摄氏温度对照表 */
void fahr2celsius()
{
    int fahr;

    printf("华氏温度和摄氏温度对照表:(300 ~ 0)\n");
    for (fahr = UPPER; fahr >= LOWER; fahr = fahr - STEP)
        printf("%3d %5.1f\n", fahr, (5.0 / 9.0) * (fahr - 32.0));
}

(2) 函数原型

函数原型是声明的一种形式。在调用子函数之前要声明子函数。

void fahr2celsius();

这种声明称为函数原型。

(3) main函数的返回值

/* p1_15.c
 * 使用子函数 - 基于p1_5.c程序
 */
#include <stdio.h>

#define LOWER 0 /* 表上限 */
#define UPPER 300 /* 表下限 */
#define STEP 20 /* 步长 */

void fahr2celsius();

int main()
{
    fahr2celsius();
    return 0;
}

/* 当华氏温度为300, 280, ..., 0时,打印华氏温度和摄氏温度对照表 */
void fahr2celsius()
{
    ......
}

main函数中的返回值是返回给执行环境的 - 用于表示用户程序的执行状态。

练习 1-16

修改打印最长文本行的程序(P.21)的主程序main,使之可以打印任意长度的输入行的长度,并尽可能地打印文本。
基于P.21中的程序实现该练习:

/* p1_16.c * 基于《TCPL》P.21程序,打印任意长度的输入行的长度,并尽可能地打印文本 */
#include <stdio.h>

#define MAXLINE 10 /* 允许输入行的最大长度 */

int mygetline(char line[], int maxline);

int main()
{
    int len;            //当前行长度
    int lmax, cmax;     /* 记录最长行的长度, 当前行的长度*/
    char line[MAXLINE]; //当前输入

    lmax = cmax = 0;
    while ((len = mygetline(line, MAXLINE)) > 0) {
        cmax    += len;
        printf("%s", line);
        if (line[len - 1] == '\n' && cmax > lmax) {
            lmax    = cmax;
            cmax    = 0;
        }
    }
    printf("%d\n", lmax);

    return 0;
}

/* mygetline函数:将一行读入到s中并返回其长度 */
int mygetline(char s[], int lim)
{
    int c, i;

    for (i = 0; i < lim - 1 && (c = getchar()) != EOF && c != '\n'; ++i)
        s[i]    = c;
    if (c == '\n') {
        s[i]    = c;
        ++i;
    }
    s[i]    = '\0';
    return i;
}
  • 函数原型与函数声明中的参数名不要求相同(参数是可选的 - 合适的参数名能够起到很好的说明性作用)。
  • 在C语言中,所有的参数都是”通过值“传递。
  • 将MAXLINE的值改为10便于测试程序。
  • 没有使用copy函数,直接读取一行后将改行输出。

在shell下简单测试p1_16.c程序:
这里写图片描述
p1_16.c程序测试

练习 1-17

编写一个程序,打印长度大于80个字符的所有输入行。

(1) 打算

简单的打算如下:
读《C程序设计语言》2E-3 [1~3]章_第11张图片

(2) 实现打算

/* p1_17.c * 打印长度大于80个字符的所有输入行 */
#include <stdio.h>

#define MINILINE 8 /* 每行超过MINILINE个字符则被打印 */
#define MORE 2
#define MAXLINE (MINILINE + MORE) /* mygetline函数每次最多统计的字符数 - 需要大于MINILINE */
#define LINE_NOT_END 1 /* 读一行字符数超过MINILINE,但未结束 */

int mygetline(char s[], int lim);

int main()
{
    int len, llen, state;
    char line[MAXLINE];

    llen    = 0;
    state   = ~LINE_NOT_END;

    while ((len = mygetline(line, MAXLINE)) > 0) {
        if (len > MINILINE || state == LINE_NOT_END) {
            printf("%s", line);
            if (line[len - 1] != '\n') state = LINE_NOT_END;
            else state = ~LINE_NOT_END;
        }
    }
    return 0;
}

/* mygetline函数:将一行读入到s中并返回其长度 */
int mygetline(char s[], int lim)
{
    int c, i;

    for (i = 0; i < lim - 1 && (c = getchar()) != EOF && c != '\n'; ++i)
        s[i]    = c;
    if (c == '\n') {
        s[i]    = c;
        ++i;
    }
    s[i]    = '\0';
    return i;
}

为在shell下对p1_17.c简单的进行测试,将80改为8:

练习 1-18

编写一个程序,删除每个输入行末尾的空格及制表符,并删除完全是空格的行。

(1) 打算

简单的打算如下:
读《C程序设计语言》2E-3 [1~3]章_第12张图片

(2) 实现打算

/* p1_18.c * 删除每个输入行末尾的空格及制表符,并删除完全是空格的行 */
#include <stdio.h>

#define IS_SPACE_OR_TAB 1 /* 为空格或制表符 */

void del_end_space_and_tab();

int main()
{
    del_end_space_and_tab();
    return 0;
}

/* 删除输入行末尾的空格和制表符, 并删除完全是空格或制表符的行 */
void del_end_space_and_tab()
{
    int c, nc, state;
    unsigned long int i, sn, tn;

    nc      = 0;
    sn = tn = 0;
    state   = ~IS_SPACE_OR_TAB;
    while ((c = getchar()) != EOF) {
        ++nc;
        if (c == ' ') {
            ++sn;
            state   = IS_SPACE_OR_TAB;
        } else if (c == '\t') {
            ++tn;
            state   = IS_SPACE_OR_TAB;
        } else {
            if (c != '\n') {
                if (state == IS_SPACE_OR_TAB) {
                    state   = ~IS_SPACE_OR_TAB;
                    for (i = 0; i < sn; ++i)
                        putchar(' ');
                    sn  = 0;
                    for (i = 0; i < tn; ++i)
                        putchar('\t');
                    tn  = 0;
                }
                putchar(c);
            } else if (c == '\n') {
                if (nc > sn + tn + 1) putchar(c);
                nc = sn = tn = 0;
            }
        }
    }
}

在shell界面下简单测试p1_18.c程序:

第一个测试输出没有和输入对齐的原因是tab输出的顺序与输入不一样而导致的对齐不同。最后一行的回车没有输入 - 空行是输入的换行。

练习 1-19

编写函数reverse(s),将字符串s中的字符顺序颠倒过来。使用该函数编写一个程序,每次颠倒一个输入行中的字符顺序。

(1) 打算

简单的打算如下:
读《C程序设计语言》2E-3 [1~3]章_第13张图片

(2) 实现打算

/* p1_19.c * 颠倒一个输入行中的字符顺序 - 舍弃超过缓存大小行的前部分字符 */
#include <stdio.h>

#define MAXLINE 1000 /* 行字符的最大数量 */

int reverse(char s[], int len);
int mygetline(char s[], int lim);

int main()
{
    int len;
    char line[MAXLINE];

    len = 0;
    while ((len = mygetline(line, MAXLINE)) > 0) {
        if (line[len - 1] == '\n') {
            reverse(line, len - 1);
            printf("%s", line);
        }
    }

    return 0;
}

/* reverse()函数:颠倒s中的字符顺序 */
int reverse(char s[], int len)
{
    int i, m, t;
    char ch;

    if (len <= 0)
        return -1;
    ch  = 0;
    t   = 0;
    m   = len / 2;
    for (i = 0; i < m; ++i) {
        t       = len - i - 1;
        ch      = s[i];
        s[i]    = s[t];
        s[t]    = ch;
    }
    return 0;
}

/* mygetline函数:将一行读入到s中并返回其长度 */
int mygetline(char s[], int lim)
{
    int c, i;

    for (i = 0; i < lim - 1 &&(c = getchar()) != EOF && c != '\n'; ++i)
        s[i]    = c;
    if (c == '\n') {
        s[i]    = c;
        ++i;
    }
    s[i]    = '\0';

    return i;
}

在shell下简单的测试p1_19.c程序:

01.14
定义与声明
在区分定义和声明的上下文中,“定义”表示创建变量或分配存储单元,而“声明”指的是变量的性质,但并不分配存储单元。

局部变量生命周期
函数中的每个局部变量只在函数被调用时存在,在函数执行完毕退出时消失(包括给函数传递的参数 - 实参保存在父函数帧栈中)。

外部变量
外部变量在程序执行期间一直存在 - 它所在的内存空间在函数执行期间没有被分配作它用。外部变量必须定义在所有函数之外,且只能定义一次,定义后编译程序将为它分配存储单元。在每个需要访问外部变量的函数中,必须声明(extern显示声明或上下文隐式声明 - 全局变量被定义在使用前)相应的外部变量 - 先声明/定义后使用。

外部变量优缺点
优:简化数据的通信 - 参数表变短了,且在需要时总可访问这些变量。
缺:过分依赖外部变量会导致一定的风险,因为它会使程序中的数据关系模糊不清 – 外部变量的值可能会意外地或不经意地修改,而程序的修改又变得十分困难。另外,在子函数中使用外部变量会让函数失去通用性。

练习 1-20

编写detab,将输入中的制表符替换成适当数目的空格,使空格充满到下一个制表符终止位的地方。假设制表符终止位的位置是固定的,比如每个n列就会出现一个制表符终止位。n应该作为变量还是符号常量呢?
变量(见分析)。

(1) 分析

简单分析如下:

(2) 实现

/* p1_20.c * 编写detab,将输入中的制表符替换成适当数目的空格,使空格充满到下一个制表符终止位的地方 */
#include <stdio.h>

#define TAB_STOP_FACTOR 8 /* 制表符停止位因子 - 制表符停止列数为该因子的倍数 */

void detab();

int main()
{
    detab();
    return 0;
}

/* detab函数:将输入中的制表符替换成适当数目的空格 - 与输入保持一致 */
void detab()
{
    int i, c, ns;
    unsigned long int nw;

    ns  = 0;
    nw  = 0;
    while ((c = getchar()) != EOF) {
        ++nw;
        if (c == '\n') {
            nw  = 0;
            putchar(c);
        } else if (c == '\t') {
            ns  = TAB_STOP_FACTOR - (nw) % TAB_STOP_FACTOR + 1;
            nw  = nw + ns - 1;
            for (i = 0; i < ns; ++i) {
                putchar(' ');
            }
        } else {
            putchar(c);
        }
    }
}

在shell下简单测试p1_20.c程序:
读《C程序设计语言》2E-3 [1~3]章_第14张图片
随意混合空格和制表符的输入。相同两行第一行为输入,第二行为输出。

练习 1-21

编写entab,将空格串替换为最少数量的制表符和空格,但要保持单词之间的间隔不变。假设制表符终止位置与练习 1-20 的detab程序的情况相同。当使用一个制表符或者一个空格都可以到达下一个制表符终止位时,选用哪一种替换字符比较好?
(若能够获取输入制表符对齐因子)使用空格或制表符都一样。(在未用制表符替换空格之前且保持单词之间间距不变)使用空格。

(1) 打算

简单分析/打算如下:
读《C程序设计语言》2E-3 [1~3]章_第15张图片

(2) 实现分析

/* p1_21.c * 编写entab,将空格串替换为最少数量的制表符和空格,但要保持单词的间隔不变。 */
#include <stdio.h>

#define TAB_STOP_FACTOR 8 /* 制表符所占字符数 */
#define IN 1 /* 在空格序列中 */
#define OUT 0 /* 不在空格序列中 */

void entab();

int main()
{
    entab();
    return 0;
}

/* entab函数:将空格串替换为最少数量的制表符和空格 */
void entab()
{
    int c, state;
    unsigned long int i, ns, nw;

    state   = OUT;
    ns = nw = 0;
    while ((c = getchar()) != EOF) {
        ++nw;
        if (c == ' ') {
            if (nw % TAB_STOP_FACTOR == 0) {
                putchar('\t');
                ns      = 0;
                state   = OUT;
            } else {
                state   = IN;
                ++ns;
            }
        } else {
            if (state == IN) {
                state   = OUT;
                for (i = 0; i < ns; ++i) putchar(' ');
                ns  = 0;
            }
            if ( c == '\n') nw  = 0;
            putchar(c);
        }
    }
}

在shell下简单测试p1_21.c程序:
这里写图片描述
用最少数量制表符和空格替换输入连续的空格且保持单词间距不变。

01.15

练习 1-22

编写一个程序,把较长的输入行“折”成短一些的两行或多行,执行的位置在输入行的第n列之前的最后一个非空格之后。要保证程序能够智能地处理输入行很长以及在指定的列前没有空格或制表符的情况。

(1) 打算

简单的打算如下:

(2) 实现打算

/* p1_22.c * 把较长行“折”成短一些的两行或多行,折行的位置在输入行的第n列之前的最后一个非空格之后。 */
#include <stdio.h>

#define MAXLINE 7 /* 若输入行长度达到该值则参考该值进行折行 */

void enter_line();
unsigned int my_getline(char buf[], const unsigned int lim);

int main()
{
    enter_line();
    return 0;
}

/* enter_line函数:根据指定长度对输入折行输出 */
void enter_line()
{
    int i;
    char buf[MAXLINE + 1];
    unsigned int len;

    len = 0;
    while ((len = my_getline(buf, MAXLINE)) > 0) {
        if (len < MAXLINE - 1 || buf[len - 1] == '\n') {
            printf("%s", buf);
        } else {
            for (i = len - 1; (buf[i] == ' ' || buf[i] == '\t') && i >= 0; --i)
                    ;
            printf("");
            if (i == len - 1) {
                buf[i + 1]  = '\n';
                buf[i + 2]  = '\0';
                printf("%s", buf);
            } else if (i < 0) {
                printf("%s", buf);
            } else {
                buf[i + 1]  = '\n';
                buf[i + 2]  = '\0';
                printf("%s", buf);
            }
        }
    }
}

/* my_getline函数:最多读入(lim - 1)个字符到buf中并返回所读到的字符的长度,buf中的字符以'\0'结尾 * 返回0时表遇文件结束 */
unsigned int my_getline(char buf[], const unsigned int lim)
{
    int c;
    unsigned int i;

    for (i = 0; i < lim - 1 && (c = getchar()) != EOF && c != '\n'; ++i)
        buf[i]  = c;
    if (c == '\n') {
        buf[i]  = '\n';
        ++i;
    }
    buf[i]  = '\0';
    return i;
}

使用变量时注意变量类型与值的对应关系。如不要给无符号整数赋予负数 - 如不应将程序中的i定义为unsigned int。

在shell下简单调试程序:
读《C程序设计语言》2E-3 [1~3]章_第16张图片
最后空行为全为空格和回车本身的换行 - 后续全是空格没有进行折行操作,直接被输出。

01.16

练习 1-23

编写一个程序删除C语言程序中的所有注释语句。同时要正确处理/识别带引号的字符串与字符常量。在C语言中,注释不允许嵌套。

(1) 打算


找到状态以及决定出进入/退出每个状态的条件,是关键。

(2) 实现打算

/* p1_23.c * 删除C程序中的注释,识别带引号的字符串和字符常量 */
#include <stdio.h>

#define INIT_STATE 0 /* 状态表初始状态 */
#define LINE_COMMENT_STATE 1 /* 在行注释状态 */
#define BLOCK_COMMENT_STATE 2 /* 在块注释状态 */
#define DOUBLE_QUOTE_STATE 3 /* 在双引号状态 */
#define SINGLE_QUOTE_STATE 4 /* 在单引号状态 */

#define TRUE 1
#define FALSE 0

void del_comment();

int main()
{
    del_comment();
    return 0;
}

/* del_comment函数:删除C程序中的注释 */
void del_comment()
{
    int c, nc, con, state;

    con     = TRUE;
    c = nc  = 0;
    state   = INIT_STATE;
    while (con && (c = getchar()) != EOF) {
        switch (c) {
            case '/':
                if (state == INIT_STATE) {
                    if ((nc = getchar()) == EOF) {
                        putchar(c);
                        con = FALSE;
                        break;
                    }
                    if (nc == '/')
                        state   = LINE_COMMENT_STATE;
                    else if (nc == '*')
                        state   = BLOCK_COMMENT_STATE;
                    else {
                        putchar(c);
                        putchar(nc);
                    }
                } else if(  state != LINE_COMMENT_STATE &&
                            state != BLOCK_COMMENT_STATE){
                    putchar(c);
                }
                break;

            case '\n':
                if (state == LINE_COMMENT_STATE) {
                    state   = INIT_STATE;
                }
                if (state != BLOCK_COMMENT_STATE)
                    putchar(c);
                break;

            case '*':
                if (state == BLOCK_COMMENT_STATE) {
                    if((nc = getchar()) == EOF) {
                        con = FALSE;
                        break;
                    }
                    if (nc == '/') state    = INIT_STATE;
                } else {
                    putchar(c);
                }
                break;

            case '\"':
                if (state == INIT_STATE) {
                    state   = DOUBLE_QUOTE_STATE;
                    putchar(c);
                } else if (state == DOUBLE_QUOTE_STATE) {
                    state   = INIT_STATE;
                    putchar(c);
                }
                break;

            case '\'':
                if (state == INIT_STATE) {
                    state   = SINGLE_QUOTE_STATE;
                    putchar(c);
                } else if (state == SINGLE_QUOTE_STATE) {
                    state   = INIT_STATE;
                    putchar(c);
                }
                break;

            default:
                if (state != LINE_COMMENT_STATE &&
                    state != BLOCK_COMMENT_STATE)
                    putchar(c);
                break;
        }
    }
}

在shell下用p1_23.c程序本身测试p1_23.c程序即:
这里写图片描述
然后打开p1_23.dat文件 - 除了比p1_23.c少了注释行外其它内容一致。

练习 1-24

编写一个程序,查找C语言程序中的基本语法错误,如圆括号、方括号、花括号不配对等。要正确处理引号(包括单引号和双引号)、转移字符序列与注释。(如果读者想把该程序编写成完全通用的程序,难度会比较大。)

(1) 打算

先留留。

(2) 实现打算

2 类型、运算符与表达式

01.16
变量名
函数/变量名(被建议和必须的)命名规则和有效长度。

数据类型
编译器可以根据硬件特性和某语言对数据类型的(硬性/选择/自主)规定自主选择数据类型的长度。不带(有符号或无符号)限定符(char类型对象)是否带符号取决于具体的机器 - 具体的机器对应一个具体的编译器。有关数据类型长度定义的符号常量以及其它与机器和编译器有关的属性可以在标准头文件limits.h和float.h中找到。

01.18
常量

  • C语言中基本类型对应的常量[整型、浮点型(后缀可表常量具体的数据类型)]
  • 常量表达式
  • 字符串常量
  • 枚举常量

P.31 - 尽管可以声明enum类型的变量,但编译器不检查这种类型的变量中存储的值是否为该枚举的有效值。不过枚举变量提供这种检查。 记录度娘的一种说法 - 编译器会检查enum变量的赋值是否是该enum类型,但不会检查赋值本身是否合法即:

/* enum.c * 验证P.31中对枚举常量变量的述说 */
#include<stdio.h>

int main()
{
    typedef enum    _boolean {
        NO,
        YES
    }boolean;   //enum类型的变量(类型)

    boolean b1, b2; //(enum)枚举变量

    b1  = (boolean)("hello\n"); //编译通过
    b2  = "hello\n";            //编译不通过
    printf("b1=%d b2=%d\n", b1, b2);
    return 0;
}

布吉岛两位翻译和审校是如何理解的?

算数运算符 关系运算符 逻辑运算符
逻辑运算符(&&;||)的优先级低于关系运算符的优先级低(>、>=、<、<=;==、!=),关系运算符优先级低于算术运算符(*、/、%;+、-)。
由&&和||连接的表达式按从左到右的顺序进行求值,并且,在知道结果为真(||)或假(&&)后立即停止计算。

01.19
类型转换
读《C程序设计语言》2E-3 [1~3]章_第17张图片

01.20
移位运算符的右操作数的值必须为非负值。
对unsigned类型的无符号值进行右移时,左边空出的部分将用0填补;当对signed类型的带符号值进行右移时,某些机器将对左边空出的部分用符号位填补(即“算术移位”),而另一些机器则对左边空出的部分用0填补(即“逻辑移位”)。 —- 移位操作由移位寄存器完成,如何设计的移位寄存器则决定移位运算的具体过程。

01.17

练习 2-1

编写一个程序以确定分别由signed及unsigned限定的char、short、int与long类型变量的取值范围。采用打印标准头文件中的相应值以及直接计算两种方式实现。后一种方法的实现较困难一些,因为要确定各种浮点类型的取值范围。

(1) 打算

读《C程序设计语言》2E-3 [1~3]章_第18张图片

(2) 实现打算

/* p2_1.c * 确定由signed和unsigned限定的char、shor、int与long类型变量的取值范围 */
#include <stdio.h>
#include <limits.h>
#include <float.h>
#include <math.h>

void data_type_range_by_header();
void data_type_range_by_calculate();

int main()
{
    data_type_range_by_header();
    data_type_range_by_calculate();
    return 0;
}

/* 查看limits.h和float.h打印各数据类型的取值范围 */
void data_type_range_by_header()
{
    printf("通过标准头文件中的符号常量看各数据类型的范围:\n类型\t最小\t\t\t最大\n");
    printf("signed:\nchar\t%d\t\t~\t%d\n",  SCHAR_MIN,  SCHAR_MAX);
    printf("short\t%d\t\t~\t%d\n",          SHRT_MIN,   SHRT_MAX);
    printf("int\t%d\t~\t%d\n",              INT_MIN,    INT_MAX);
    printf("long\t%ld\t~\t%ld\n",           LONG_MIN,   LONG_MAX);
    printf("float\t%f\t~\t%f\n",            -FLT_MAX,   FLT_MAX);
    printf("double\t%f\t~\t%f\n",           -DBL_MAX,   DBL_MAX);

    printf("\nunsigned:\nchar\t%u\t~\t%u\n",    0, UCHAR_MAX);
    printf("short\t%u\t~\t%u\n",                0, USHRT_MAX);
    printf("int\t%u\t~\t%u\n",                  0, UINT_MAX);
    printf("long\t%u\t~\t%lu\n",                0, ULONG_MAX);
}

/* 根据整型补码和IEEE浮点数在内存中的表示计算每种类型的浮点数范围 */
void data_type_range_by_calculate()
{
    printf("\n通过数据类型在计算机中的编码方式计算各数据类型的范围:\n类型\t最小\t\t\t最大\n");
    printf("signed:\nchar\t%d\t\t~\t%d\n",  -(int)              pow(2, 8 * sizeof(char)     - 1),   \
                                            (int)   (pow(2, 8 * sizeof(char)    - 1) - 1));
    printf("short\t%d\t\t~\t%d\n",          -(int)              pow(2, 8 * sizeof(short)    - 1),   \
                                            (int)   (pow(2, 8 * sizeof(short)   - 1) - 1));
    printf("int\t%d\t~\t%d\n",              -(unsigned)         pow(2, 8 * sizeof(int)      - 1),   \
                                            (int)   (pow(2, 8 * sizeof(int)     - 1) - 1));
    printf("long\t%ld\t~\t%ld\n",           -(unsigned long)    pow(2, 8 * sizeof(long)     - 1),   \
                                            (long int)(pow(2, 8 * sizeof(long)  - 1) - 1));
    printf("短实数\t%Lf\t~\t%Lf\n",         -powl(2, 104) * (pow(2, 24) - 1),                       \
                                            powl(2, 104) * (pow(2, 24) - 1));
    printf("长时数\t%Lf\t~\t%Lf\n",         -powl(2, 2046 - 1023 - 52) * (powl(2, 53) - 1),         \
                                            powl(2, 2046 - 1023 - 52) * (powl(2, 53) - 1));

    printf("\nunsigned:\nchar\t%u\t~\t%u\n",0,  (unsigned char)     (pow(2, 8 * sizeof(char))   - 1));
    printf("short\t%u\t~\t%u\n",            0,  (unsigned short)    (pow(2, 8 * sizeof(short))  - 1));
    printf("int\t%u\t~\t%u\n",              0,  (unsigned int)      (pow(2, 8 * sizeof(int))    - 1));
    printf("long\t%u\t~\t%lu\n",            0,  (unsigned long)     (pow(2, 8 * sizeof(long))   - 1));
}

在shell下调试、连接p2_1.c并运行相应的可执行文件。
读《C程序设计语言》2E-3 [1~3]章_第19张图片

01.18

练习 2-2

在不使用运算符&&和||的条件下编写一个与上面(P.32)for循环语句等价的循环语句。
分析:分析该for语句含义(执行步骤)用关系运算符实现(等价 - C语句含义层面)。
实现。

for (i = 0; i < lim - 1; ++i)
    if ((c = getchar()) != '\n') 
        if (c != EOF)
            s[i]    = c;

01.19

练习 2-3

编写htoi(s),把由16进制数字组成的字符串(包含可选的前缀0x或0X)转换为与之等价的整型值。字符串允许包含的数字包括:0 ~ 9、a ~ f以及A ~ F。

打算

简单打算如下。
在ASCII字符集中,(大写字母与对应的小写字母作为数字值来说具有固定的间隔,并且)每个字母表都是连续的 ~ 即A ~ Z之间只有字母。但是,后面一点(连续)对EBCDIC字符集是不成立的,因此用大于等于’a’/’A’小于等于’f’/’F’的做法可能(没向度娘询问EBCDIC字符集编码)并不能保证期间的字符都为16进制对应的字母。

实现打算

/* p2_3.c * 把由16进制数字组成的字符串转换为与之等价的整型值 */
#include <stdio.h>
#include <ctype.h>

#define HEXSTR "0X00000001"
#define OSNBIT 64

long int htoi(const char s[]);

int main()
{
    printf("%ld\n", htoi(HEXSTR));
    return 0;
}

/* htoi(s)函数:把由16进制数字组成的字符串(包含可选的前缀0x或0X)转换为与之等价的整型值 */
long int htoi(const char s[])
{
    unsigned char   i, xch;
    long int        dec;

    i = xch = 0;
    dec     = 0;
    if (s[0] == '0' && (s[1] == 'X' || s[1] == 'x'))
        i   += 2;

    for (; isxdigit(s[i]); ++i) {
        if (i >= OSNBIT / 8 + 2) break;

        xch = s[i];
        if (isdigit(xch)) {
            dec = 16 * dec +  xch - '0';
        } else if(isalpha(xch)) {
            dec = 16 * dec + xch - 'a';
        } else {
            break;
        }
    }
    return dec;
}

练习 2-4

重新编写squeeze(s1, s2)[P.37],将字符串s1中任何与字符串s2中字符匹配的字符都删除。
简单版本的代码如下:

/* p2_4.c * 编写squeeze(s1, s2)函数,将字符串s1中任何与字符串s2中字符匹配的字符都删除 */
#include <stdio.h>

void squeeze_char(char s[], int ch);
void squeeze_repeat(char s[]);
void squeeze(char s1[], char s2[]);

int main()
{
    char s1[] = "i love you once i love you twice i love you more than bean and rice";
    char s2[] = "iiiiiiiilove love you you";

    printf("s1=%s\ns2=%s\n", s1, s2);
    squeeze(s1, s2);
    printf("s1=%s\n", s1);
    return 0;
}

/* squeeze_char(s, c)函数:删除字符串s中的c字符 * O(n^2) */
void squeeze_char(char s[], int ch)
{
    int i, j;

    if (s == NULL) return;
    for (i = j = 0; s[i] != '\0'; ++i) {
        if (s[i] != ch) s[j++]  = s[i];
    }
    s[j]    = '\0';
}

/* squeeze_repeat(s)函数:消除s字符串中的重复的字符 * O(n^2) */
void squeeze_repeat(char s[])
{
    int i, j, k;

    if (s == NULL) return;
    for (i = 0; s[i] != '\0'; ++i) {
        k       = i;
        s[k++]  = s[i];

        for (j = i + 1; s[j] != '\0'; ++j) {
            if (s[j] != s[i]) s[k++]    = s[j];
        }
        s[k]    = '\0';
    }
}

/* squeeze(s1, s2)函数:将字符串s1中任何与字符串s2中字符匹配的字符都删除 * s2为非const类型 * O(n^2) */
void squeeze(char s1[], char s2[])
{
    int i;

    if (s1 == NULL || s2 == NULL) return;
    squeeze_repeat(s2);
    for (i = 0; s2[i] != '\0'; ++i) {
        squeeze_char(s1, s2[i]);
    }
}
  • 无论是否进行符号扩展,字符型变量都将被转换成整型变量。所以直接将函数的字符型变量声明为整型(传参时本会按照参数类型进行强制类型转换)。
  • 程序中将O(n^2)复杂度拆分成两个O(n^2)复杂度。

在shell界面下简单测试p2_4.c程序:
这里写图片描述

练习 2-5

编写函数any(s1, s2),将字符串s2中任一字符在字符串s1中第一次出现的位置作为结果返回。如果s1中不包含s2中的字符,则返回-1。(标准库函数strpbrk具有同样的功能,但它返回的是指向该位置的指针)。
简单版本的代码如下:

/* p2_5.c * 编写函数any(s1, s2) - 将字符串s2中的任一字符在字符串s1中第一次出现的位置作为结果返回, * 如果s1中不包含s2中的字符,则返回-1 */
#include <stdio.h>

void squeeze_repeat(char s[]);
int any_char(char s[], int ch);
int any(char s1[], char s2[]);

int main()
{
    char s1[] = "hello 2016";
    char s2[] = "hello 2016";
    char s3[] = "hello 2016";
    char s4[] = "6102 hello";
    char s5[] = "hello 2016";
    char s6[] = "vast";

    printf("%d\n", any(s1, s2));
    printf("%d\n", any(s3, s4));
    printf("%d\n", any(s5, s6));
    return 0;
}

/* squeeze_repeat(s)函数:消除s字符串中的重复的字符 * O(n^2) */
void squeeze_repeat(char s[])
{
    int i, j, k;

    if (s == NULL) return;
    for (i = 0; s[i] != '\0'; ++i) {
        k       = i;
        s[k++]  = s[i];

        for (j = i + 1; s[j] != '\0'; ++j) {
            if (s[j] != s[i]) s[k++]    = s[j];
        }
        s[k]    = '\0';
    }
}

/* any_char(s, ch)函数:返回ch在s中首次出现的位置;若ch在s中则返回-1 */
int any_char(char s[], int ch)
{
    int i;

    for (i = 0; s[i] != '\0'; ++i)
        if (s[i] == ch) return i;
    return -1;
}

int any(char s1[], char s2[])
{
    int i, r;

    if (s1 == NULL || s2 == NULL) return;

    r   = -1;
    squeeze_repeat(s2);
    for (i = 0; s2[i] != '\0'; ++i) {
        if ((r = any_char(s1, s2[i])) != -1)
            return r;
    }
    return -1;
}

在shell界面中简单测试p2_5.c程序:
这里写图片描述

01.20

练习 2-6

编写一个函数setbits(x, p, n, y),该函数返回对x执行下列操作后的结果值:将x中从第p位开始的n个(二进制)位设置为y中最右边n位的值,x的其余各位保持不变。

(1) 打算

简单打算如下:

(2) 实现打算

简单实现打算如下:

/* p2_6.c * 编写函数setbits(x, p, n, y): * 将x中从第p位开始的n个(二进制)位设置为y中最右边n位的值,x的其余位保持不变。 */
#include <stdio.h>

unsigned int setbits(unsigned x, unsigned y, int p, int n);

int main()
{
    unsigned int x, y;
    int p, n;

    x   = 0x00000000;
    y   = 0xffffffff;
    p   = 2;
    n   = 2;

    printf("x=%x y=%x\n", x, y);
    printf("x=%x y=%x\n", setbits(x, y, p, n), y);
    return 0;
}

unsigned int setbits(unsigned x, unsigned y, int p, int n)
{
    int         nbit;
    unsigned    by;

    nbit    = sizeof(x) * 8;
    if (p < 1 || p > nbit || n < 0 || p + n > nbit) return 0;
    x   = x | ((~(~0 << n)) << (p - 1));
    by  = ~(~0 << (p - 1)) | ~(~0 << nbit - 1 - p - 1 - n + 1) << (p + n) | (y << p - 1);
    return x & by;
}

在shell下简单调试p2_6.c程序:

01.21

练习 2-7

编写一个函数invert(x, p, n),该函数返回对x执行下列操作后的结果值:将x中从第p位开始的n个(二进制)位求反(即,1变成0,0变成1),x的其余各位保持不变。

(1) 打算

简单的打算如下:

(2) 实现打算

简单的实现打算:

/* p2_7.c * 编写函数invert(x, p, n): * 将x中从第p位开始的n个(二进制)位求反(即1变成0, 0变成1),x的其余各位保持不变。 * 该函数返回经求反的x。 */
#include <stdio.h>

unsigned invert(unsigned x, int p, int n);

int main()
{
    int         p, n;
    unsigned    x;

    x   = 0xffffffff;
    p   = 1;
    n   = 1;
    printf("x=%x\n", invert(x, p, n));
    return 0;
}

unsigned invert(unsigned x, int p, int n)
{
    int         nbit;
    unsigned    bx;

    nbit    = sizeof(x) * 8;
    if (1 > p || p > nbit || 1 > n || n > nbit || p + n > nbit + 1)
        return 0;

    bx  = 0;
    if (n == nbit) bx =0xffffffff;  //移位操作数超过nbit - 1后(~(~0 << n) << p - 1)表达式不再正确
    else bx = bx | (~(~0 << n) << p - 1);
    return x ^ bx;
}

在shell界面下运行p2_7.c程序(对应的可执行文件):
x=fffffffe

练习 2-8

编写一个函数rightrot(x, n),该函数返回将x循环右移(即从最右端移出的位将从最左端移入)n(二进制)位后所得到的的值。

(1) 打算

简单打算如下:

(2) 实现打算

简单实现打算:

/* p2_8.c * 编写rightrot(x, n)函数: * 返回将x循环右移n位后所得到的值。 */
#include <stdio.h>

unsigned rightrot(unsigned x, unsigned n);

int main()
{
    unsigned    x;
    unsigned    n;

    x   = 0x00ffffff;
    n   = 8;
    printf("x=%x\n", x);
    printf("x=%x\n", rightrot(x, n));
    return 0;
}

unsigned rightrot(unsigned x, unsigned n)
{
    int         nbits;
    unsigned    bx;

    bx  = 0;
    nbits   = sizeof(x) * 8;
    if (n == 0 || n >= nbits)
        return x;
    bx  = x & ~(~0 << n);
    bx  = bx << nbits - 1 - n + 1;
    return x | bx;
}

在shell下简单运行p2_8.c程序(对应的可执行文件):
x=ffffff
x=ffffffff

练习 2-9

在求对二的补码(Two’s complement)时,表达式x &=(x - 1)可以删除x中最右边值为1的一个二进制位(如1000中的1)。请解释这样做的道理。用这一方法重写bitcount函数(P.40 ~ P.41),以加快其执行速度。

(1) 分析

理解题目中的“这样”:指x &=(x - 1)删除x中最右边值为1的一个二进制位。
读《C程序设计语言》2E-3 [1~3]章_第20张图片

(2) bitcount(x)

/* p2_9.c * 编写bitcount(x)函数:求x中二进制位为1的数量 */
#include <stdio.h>
unsigned bitcount(unsigned x);

int main()
{
    unsigned x;

    x   = 0xfffffff0;
    printf("%u\n", bitcount(x));
    return 0;
}

unsigned bitcount(unsigned x)
{
    unsigned nb;

    nb  = 0;
    if (x == 0) return nb;
    while (x &= (x - 1)) {
        ++nb;
    }
    ++nb;
    return nb;
}

在shell下运行p2_9.c程序(对应的可执行文件):
28

练习 2-10

重新编写将大写字母转换为小写字母的函数lower(P.34),并用条件表达式替换其中的if-else结构。

/* p2_10.c * 用条件表达式实现lower函数 - 将大写字母转换为小写字母 */
#include <stdio.h>
#include <ctype.h>

int lower(int c);

int main()
{
    printf("%c\n", lower('A'));
    return 0;
}

int lower(int c)
{
    return isupper(c) ? c + 'a' - 'A' : c;
}

在shell界面下运行p2_10.c(对应的可执行文件):
a

01.25

3 控制流

练习 3-1

在上面(P.46)有关折半查找的例子中,while循环语句内共执行了两次测试,其实只要一次就足够(代价是将更多的测试在循环外执行)。重写该函数,使得在循环内部只执行一次测试。比较两个版本函数的运行时间。

(1) 分析

简单分析如下:
对二分查找进行理解,对low <= high、对当前元素值的判断以及判断后设置下标这些语句必不可少。进一步简单的分析如下:
读《C程序设计语言》2E-3 [1~3]章_第21张图片

(2) 实现

/* p3_1.c
 * 对if-else-if语句理解/验证
 */
/* P.57
 * binsearch函数:在v[0] <= v[1] <= v[2] <= ... <= v[n-1] */
int binsearch(int x, int v[], int n)
{
    int low, high, mid;

    low     = 0;
    high    = n - 1;
    while (low <= high) {
        mid = (low + high) / 2;
        if (x < v[mid])
            high    = mid - 1;
        else if (x > v[mid])
            low     = mid + 1;
        else
            return mid;
    }
    return -1;
}

/* 只在循环内进行一次判断的二分查找
 * 瞄了一眼答案 - 未验证
 */
int binsearchv2(int x, int v[], int n)
{
    int low, high, mid;

    low     = 0;
    high    = n - 1;
    mid     = (low + high) / 2;
    while (low <= high && x != v[mid]) {
        if (x < v[mid])
            high    = mid - 1;
        else
            low     = mid + 1;
        mid = (low + high) / 2;
    }
    if (x == v[mid])
        return mid;
    else
        return -1;
}

不用轻易动摇看答案,仔细分析有了思路后再用语言这个工具演绎。

比较两个版本的二分查找函数,从数据结构中瞄到的O分析法来看,二者一样。从更细微的角度看,两个函数都进行了相同次数的判断测试(循环条件测试 + 循环语句中的测试),也未多增或多减其它语句,故而两个函数的运行时间应该来说是半径八两、难分高下。

01.26

练习3-2

编写一个函数escape(s, t),将字符串t复制到字符串s中,并在复制过程中将换行符、制表符等不可见字符分别换成\n、\t等相应的可见的转义字符序列。要求使用switch语句。再编写一个具有相反功能的函数,在复制过程中将转义字符序列转换为实际字符。

简单的实现题目要求如下:

/* p3_2.c * 编写一个函数escape(s, t) - * 将字符串t复制到字符串s中, * 并在复制过程中将换行符等不可见字符分别转换为\n、\t等相应的可见的转义字符序列, * 要求使用switch语句; * 再编写一个具有相反功能的函数,在复制过程中将转义字符序列转换为实际的字符。 */
#include <stdio.h>

void escape(char s[], char t[]);
void face(char d[], char s[]);
int main()
{
    char s[100], d[100];
    char t[] = "hello \n 2016";

    escape(s, t);
    printf("t-%s\ns-%s\n", t, s);
    face(d, s);
    printf("s-%s\nd-%s\n", s, d);
    return 0;
}

void escape(char s[], char t[])
{
    unsigned i, j;

    j   = 0;
    for (i = 0; t[i] != '\0'; ++i) {
        switch (t[i]) {
            case '\n':
                s[j++]  = '\\';
                s[j++]  = 'n';
                break;
            case '\t':
                s[j++]  = '\\';
                s[j++]  = 't';
                break;
            default:
                s[j++]  = t[i];
                break;
        }
    }
    s[j]    = '\0';
}

void face(char d[], char s[])
{
    unsigned i, j;

    j   = 0;
    for (i = 0; s[i] != '\0'; ++i) {
        switch (s[i]) {
            case '\\':
                switch (s[i + 1]) {
                    case 'n':
                        ++i;
                        d[j++]  = '\n';
                        break;
                    case 't':
                        ++i;
                        d[j++]  = '\t';
                        break;
                    //其它转义字符序列
                    default:
                        d[j++]  = s[i];
                        break;
                }
                break;
            default:
                d[j++]  = s[i];
                break;
        }
    }
    d[j]    = '\0';
}

在shell下简单的测试p3_2.c:
读《C程序设计语言》2E-3 [1~3]章_第22张图片

练习 3-3

编写函数expand(s1, s2),将字符串s1中类似于a-z一类的速记符号在字符串s2中扩展为等价的完整列表abc…xyz。该函数可以处理大小写字母和数字,并可以处理a-b-c、a-z0-9与-a-z等类似的情况。作为前导和尾随的-字符原样排印。

(1) 打算

简单分析/打算如下:

(2) 实现打算

简单实现打算如下:

/* p3_3.c * 编写函数expand(s1, s2) - * 将字符串s1中类似于a-z一类的速记符号在字符串s2中扩展为等价的完整列表abc...xyz; * 该函数可以处理大小写字母和数字,并可以处理a-b-c、a-z0-9与-a-z等类似的情况; * 作为前导和尾随的-字符原样排印。 */
#include <stdio.h>

void expand(char s1[], char s2[]);
/* 简单测试expand(s1, s2)函数 */
int main()
{
    char s1[] = "-a-za-z0-9a-b-c-";
    char s2[100] = "";

    expand(s1, s2);
    printf("s1=%s\ns2=%s\n", s1, s2);
    return 0;
}

void expand(char s1[], char s2[])
{
    unsigned i, j, k;
    unsigned chf, chn;

    j   = 0;
    chf = chn = 0;
    for (i = 0; s1[i] != '\0'; ++i) {
        if (s1[i] != '-' || i == 0) {
            s2[j++] = s1[i];
        } else {
            chf = s1[i - 1], chn = s1[i + 1];
            if (    (isdigit(chf) && isdigit(chn))  ||  \
                    (isupper(chf) && isupper(chn))  ||  \
                    (islower(chf) && islower(chn))      ) {

                for (k = 0; k < chn - chf - 1; ++k)
                    s2[j++] = chf + k + 1;
                s2[j++] = chn;
                ++i;
            } else {
                s2[j++] = s1[i];
            }
        }
    }
    s2[j]   = '\0';
}

在shell下简单测试p3_3.c:
这里写图片描述

练习 3-4

在数的对二补码表示中,我们编写的itoa函数(P.52-53)不能处理最大的负数,即n等于-(2^字长-1)的情况。请解释原因。修改该函数,使它在任何机器上运行时都能打印出正确的值。

(1) 分析

简单分析如下。
P.52-53中的itoa函数代码如下:

/* itoa函数:将数字n转换为字符串并保存到s中 */
void itoa(int n, char s[])
{
    int i, sign;
    if ((sign = n) < 0)
        n = -n;
    i   = 0;
    do {
        s[i++] = n % 10 + '\0';
    } while ((n /= 10) > 0);
    if (sign < 0)
        s[i++] = '-';
    s[i] = '\0';
    reverse(s);

若n = INT_MIN(最小的负数)时,由于n为有符号数,故而n =-n语句得到的结果并不是n等于最小负数的相反数。用unsigned类型的变量可以保存int任何的负数的相反数。

(2) 实现

/* p3_4.c * 编写itoa(n, s)函数 - * 将数字n转换为为字符串并保存到s中。 */
#include <stdio.h>
#include <limits.h>
#include <string.h>

void reverse(char s[]);
void itoa(int n, char s[]);
/* 简单测试itoa(n, s)函数 */
int main()
{
    char s[100];

    itoa(INT_MIN, s);
    printf("INT_MIN=%d\ns=%s\n", INT_MIN, s);
    return 0;
}

void itoa(int n, char s[])
{
    int         i, sign;
    unsigned    bn;

    if ((sign = n) < 0)
        bn  = -n;
    else bn = n;

    i   = 0;
    do {
        s[i++]  = bn % 10 + '0';
    }while ((bn /= 10) > 0);

    if (sign < 0)
        s[i++]  = '-';
    s[i]    = '\0';
    reverse(s);
}

/* reverse(s)函数:倒置字符串s中各个字符的位置 * 改于p1_19.c */
void reverse(char s[])
{
    int     i, t, ch;
    size_t  m, len;

    t   = ch = 0;
    len = strlen(s);
    m   = len / 2;
    for (i = 0; i < m; ++i)
        t = len - i - 1, ch = s[i], s[i] = s[t], s[t] = ch;
}

在shell下简单测试p3_4.c:
这里写图片描述

01.27

练习 3-5

编写函数itob(n, s, b),将整数n转换为以b为底的数,并将转换结果以字符的形式保存到字符串s中。例如,itob(n, s, 16)把整数n格式化成十六进制整数保存在s中。

(1) 打算

简单打算如下:
读《C程序设计语言》2E-3 [1~3]章_第23张图片

(2) 实现打算

简单实现打算如下:

/* p3_6.c * 编写函数itob(n, s, b) - * 将整数n转换为以b为底的数,并将转换结果以字符的形式保存到字符串s中。 */
#include <stdio.h>
#include <string.h>

void reverse(char s[]);
void itob(unsigned n, char s[], unsigned b);
/* 简单测试itob(n, s, b)函数 */
int main()
{
    unsigned n, b;
    char s[100];

    n   = 0, b  = 8;
    itob(n, s, b);
    printf("n=%u\ts=%s\n", n, s);

    n   = 65109, b = 16;
    itob(n, s, b);
    printf("n=%u\ts=%s\n", n, s);

    n   = 3, b  = 3;
    itob(n, s, b);
    printf("n=%u\ts=%s\n", n, s);
    return 0;
}
void itob(unsigned n, char s[], unsigned b)
{
    unsigned i, rm;

    i   = 0;
    if (!b) {
        s[i++]  = '0';
        s[i]    = '\0';
        return;
    }

    do {
        rm      = n % b;
        if (rm < 10) rm += '0';
        else    rm += 55;   //根据ASCII表,让大于10的数用从‘A’开始的字符表示
        s[i++]  = rm;
    } while((n = n / b) >= b);

    if (n < 10) n   += '0';
    else n  += 55;
    s[i++]  = n;
    s[i]    = '\0';
    reverse(s);
}

/* reverse(s)函数:倒置字符串s中各个字符的位置 */
void reverse(char s[])
{
    int     i, t, ch;
    size_t  m, len;

    t   = ch = 0;
    len = strlen(s);
    m   = len / 2;
    for (i = 0; i < m; ++i)
        t = len - i - 1, ch = s[i], s[i] = s[t], s[t] = ch;
}

在shell下简单测试p3_5.c:

练习 3-6

修改itoa函数,使得该函数可以接收三个参数。其中,第三个参数为最小字段宽度。为了保证转换后所得的结果至少具有第三个参数指定的最小宽度,在必要时应在所得结果的左边填充一定的空格。

简单的实现如下:

/* p3_6.c * 修改itoa函数(p3_4.c) - * 使得该函数可以接收三个参数。其中第三个参数为最小字段宽度。 * 为了保证转换后所得的结果至少具有第三个参数指定的最小宽度, * 在必要时应在所得结果的左边填充一定的空格。 */
#include <stdio.h>
#include <limits.h>
#include <string.h>

void reverse(char s[]);
void itoa(int n, char s[], int w);
/* 简单测试itoa(n, s, w)函数 */
int main()
{
    char s[100];
    int n, w;

    n   = 1;
    w   = 5;
    itoa(n, s, w);
    printf("n=%d\ns=%s\n", n, s);

    itoa(INT_MIN, s, w);
    printf("\nn=%d\ns=%s\n", INT_MIN, s);
    return 0;
}

void itoa(int n, char s[], int w)
{
    int         i, sign;
    unsigned    bn;
    size_t      len;

    //if (w > screen_width) w = screen_width - strlen(s);
    if ((sign = n) < 0)
        bn  = -n;
    else bn = n;

    i   = 0;
    do {
        s[i++]  = bn % 10 + '0';
    } while ((bn /= 10) > 0);

    if (sign < 0)
        s[i++]  = '-';

    len = strlen(s);
    if (w > (int)len) { //避免负的int转为unsigned的隐式转换
        int j, sn;

        sn  = w - (int)len;
        for (j = 0; j < sn; ++j)
            s[i++]  = ' ';
    }
    s[i]    = '\0';
    reverse(s);
}

void reverse(char s[])
{
    int     i, t, ch;
    size_t  m, len;

    t   = ch = 0;
    len = strlen(s);
    m   = len / 2;
    for (i = 0; i < m; ++i)
        t = len - i - 1, ch = s[i], s[i] = s[t], s[t] = ch;
}

在shell下简单测试p3_6.c:
读《C程序设计语言》2E-3 [1~3]章_第24张图片

4 练字

乱糟糟,似没达效果。

01.22
2 类型、运算符和表达式

明白现象出现的道理比理解现象本身更好些。
读《C程序设计语言》2E-3 [1~3]章_第25张图片

01.25
3 控制流

笔记

读《C程序设计语言》(2E,中文版)第1 ~ 3章的练习题笔记保存地址:tcpl_practice_ch1-3。

[2016.01.07 - 18:10]

你可能感兴趣的:(C语言,TCPL)