《C程序设计语言》学习笔记

第1章 导言

1.1 入门

打印hello world

#include <stdio.h> // 告诉编译器在本程序中包含标准输入/输出库的信息

main() { // 定义main函数,它不接受参数值
    printf("hello, world\n"); // 将"hello, world\n"作为参数调用库函数printf方法显示字符
}

一个C语言程序,无论其大小如何,都是由函数和变量组成的。函数中包含一些语句,以指定所要执行的计算操作;变量则用于存储计算过程中使用的值。main是一个特殊的函数名——每个程序都从main函数的起点开始执行,这意味着每个程序都必须在某个位置包含一个main函数。

在C语言中,字符序列\n表示换行符,在打印中遇到它时,打印输出将换行,从下一行的左端行首开始。C语言提供的转义字符还包括:\t制表符;\b回退符;"表示双引号;\表示反斜杠本身。

1.2 变量与算术表达式

#include <stdio.h>

int main()
{
    int fahr, celsius;
    int lower, upper, step;

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

    fahr = lower;
    while (fahr <= upper)
    {
        celsius = 5 * (fahr - 32) / 9; // 由于celsius是整数类型,除法操作将执行舍位。
        printf("%d\t%d\n", fahr, celsius); // printf中参数的各个%对应其他的参数之一进行替换的位置,并制定打印格式,它们在数目和类型上都必须匹配,否则将出现错误的结果。
      	printf("%3d %6d\n", fahr, celsius); // 可以在%d中指明打印宽度
        fahr = fahr + step;
    }
}

在C语言中,所有变量必须先声明后使用。声明通常放在函数起始处,在任何可执行语句之前。声明用于说明变量的属性,它由一个类型名和一个变量表组成。

除了int与float类型之外,C语言还提供其他一些基本数据类型,例如:

  • char 字符-一个字节
  • short 短整型
  • long 长整型
  • double 双精度浮点型

这些数据类型对象的大小取决于具体的机器。

printf函数不是C语言本身的一部分,C语言本身并没有定义输入/输出功能,printf仅仅是标准库函数中一个有用的函数。ANSI标准定义了标准库的行为,因此,对每个符合该标准的编译器和库来说,该函数的属性都是相同的。

#include <stdio.h>

// 浮点数方式
int main()
{
    float fahr, celsius;
    int lower, upper, step;

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

    fahr = lower;
    while (fahr <= upper)
    {
        celsius = (5.0/9.0) * (fahr-32.0);
        printf("%3.0f %6.1f\n", fahr, celsius); // %3.0f表示浮点数至少占3个字符宽,且不带小数点和小数部分;
        fahr = fahr + step;
    }
}

如果某个算术运算符的所有操作数均为整型,则执行整型运算。但是,如果某个算术运算符有一个浮点型操作数和一个整型操作数,则在开始运算之前整型操作符将会被转换成浮点型。

  • %d 按照十进制整数打印
  • %6d 按照十进制整数打印,至少6个字符宽
  • %f 按照浮点数打印
  • %6f 按照浮点数打印,至少6个字符宽
  • %.2f 按照浮点数打印,小数点后有两位小数
  • %6.2f 按照浮点数打印,至少6个字符宽,小数点后有两位小数

printf还支持下列格式:

  • %o 表示八进制数
  • %x 表示十六进制数
  • %c 表示字符
  • %s 表示字符串
  • %% 表示百分号本身

1.3 for语句

#include <stdio.h>

int main()
{
    int fahr;

    for (fahr = 0; fahr <= 300; fahr = fahr + 20)
        printf("%3d %6.1f\n", fahr, (5.0 / 9.0) * (fahr - 32));
}

for语句是一种循环语句,它是对while语句的推广。与while语句一样,for循环语句的循环体可以只有一条语句,也可以是用花括号括起来的一组语句。初始化部分(第一部分)、条件部分(第二部分)与增加步长部分(第三部分)都可以是任何表达式。

1.4 符号常量

在程序中使用300、20等类似的“幻数”并不是一个好习惯,它们几乎无法向以后阅读该程序的人提供什么信息,而且使程序的修改变得更加困难。处理这种幻数的一种方法是赋予它们有意义的名字,#define指令可以把符号名(或称为符号常量)定义为一个特定的字符串:#define 名字 替换文本

#include <stdio.h>

#define LOWER 0 // 定义常量,不用带分号
#define UPPER 300
#define STEP 20

int main()
{
    int fahr;

    for (fahr = LOWER; fahr <= UPPER; fahr = fahr + STEP)
        printf("%3d %6.1f\n", fahr, (5.0 / 9.0) * (fahr - 32));
}

1.5 字符输入/输出

文本流由多行字符构成的字符序列,而每行字符则由0个或多个字符组成,行末是一个换行符。

标准库提供了一次读/写一个字符的函数,其中最简单的是getchar和putchar两个函数。每次调用时,getchar函数从文本流中读入下一个输入字符,并将其作为结果值返回。

1.5.1 文件复制
#include <stdio.h>

// 版本1
main()
{
    int c;

    c = getchar();
    while (c != EOF)
    {
        putchar(c);
        c = getchar();
    }
}

//版本2
main()
{
    int c;

    while ((c = getchar()) != EOF)
        putchar(c);
}

在没有输入时,getchar将返回一个特殊值,EOF(end of file,文件结束)。

1.5.2 字符计数
#include <stdio.h>

// 版本2
main()
{
    long nc; // 长整型占32位存储单元

    nc = 0;
    while (getchar() != EOF)
    {
        ++nc;
    }
    printf("%ld\n", nc);
}

// 版本2
main()
{
    double nc; // double类型可以处理更大的数字

    for (nc = 0; getchar() != EOF; nc++)
        ; // C要求for循环语句必须有一个循环体,因此用单独的分号代替。单独的分号称为空语句。
    printf("%.0f\n", nc);
}
1.5.3 行计数
#include <stdio.h>

main()
{
    int c, nl;

    nl = 0;
    while ((c = getchar()) != EOF)
    {
        if (c == '\n') // 单引号中的字符表示一个整型值,即ASCII值,该值等于此字符在机器字符集中对应的数值,我们称之为字符常量
            ++nl;
    }
    printf("%d\n", nl);
}
1.5.4 单词计数
#include <stdio.h>

#define IN 1
#define OUT 0

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

    state = OUT;
    nl = nw = nc = 0; // 赋值语句是从右至左,相当于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);
}

1.6 数组

// 数组统计字符
#include <stdio.h>

int main()
{
    int c, i, nwhite, nother;
    int ndigit[10];

    nwhite = nother = 0;
    for (i = 0; i < 10; ++i)
    {
        ndigit[i] = 0;
    }

    while ((c = getchar()) != EOF)
    {
        if (c >= '0' && c <= '9')
            ++ndigit[c - '0'];
        else if (c == ' ' || c == '\n' || c == '\t')
            ++nwhite;
        else
            ++nother;
    }

    printf("digits =");
    for (i = 0; i < 10; ++i)
        printf(" %d", ndigit[i]);
    printf(", white space = %d, other = %d\n", nwhite, nother);
}

1.7 函数

函数定义可以以任意次序出现在一个源文件或多个源文件中,但同一函数不能分割存放在多个文件中。

函数定义中圆括号内列表中出现的变量称为形参,函数调用中与形参对应的值称为实参。

main函数的调用者是程序的执行环境,返回0代表正常终止,返回非0表示出现异常情况或出错结束条件。

// 求幂
#include <stdio.h>

int power(int m, int n); // 函数原型

int main(void)
{
    int i;

    for (i = 0; i < 10; ++i)
        printf("%d %d %d\n", i, power(2, i), power(-3, i));
    return 0;
}

int power(int base, int n)
{
    int i, p;

    p = 1;
    for (i = 1; i <= n; ++i)
    {
        p = p * base;
    }
    return p;
}

1.8 参数——传值调用

在C语言中,所有函数参数都是“通过值”传递的。传递给被调用函数的参数值存放在临时变量中,而不是存放在原来的变量中。这与其他某些语言是不同的,比如,Fortran等语言是“通过引用调用”,被调用的函数必须访问原始参数,而不是访问参数的本地副本。

最主要的区别在于,在C语言中,被调用函数不能直接修改主调函数中变量的值,而只能修改其私有的临时副本的值。

必要时,也可以让函数能够修改主调函数中的变量。这种情况下,调用者需要向被调用函数提供待设置值的变量的地址(变量的指针),而被调用函数则需要将对应的参数声明为指针类型,并通过它间接访问变量。

数组参数的情况有所不同,当把数组名用作参数时,传递给函数的值是数组起始元素的位置或地址——它并不复制数组元素本身。

1.9 字符数组

字符数组是C语言中最常用的数组类型。

#include <stdio.h>

#define MAXLINE 1000

int getline1(char line[], int maxline);
void copy(char to[], char form[]);

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

    max = 0;
    while ((len = getline1(line, MAXLINE)) > 0)
    {
        if (len > max)
        {
            max = len;
            copy(longest, line);
        }
    }
    if (max > 0)
        printf("%s", longest);
    return 0;
}

int getline1(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;
}

void copy(char to[], char from[])
{
    int i;

    i = 0;
    while ((to[i] = from[i]) != '\0')
        ++i;
}

1.10 外部变量与作用域

函数中的每个局部变量只在函数被调用时存在,在函数执行完毕退出时消失。

在每个需要访问外部变量的函数中,必须声明相应的外部变量。声明时可以用extern语句显式声明,也可以通过上下文隐式声明。函数在使用外部变量之前,必须要知道外部变量的名字。在源文件中,如果外部变量定义出现在使用它的函数之前,那么在那个函数中就没有必要使用extern声明。

如果程序包含在多个源文件中,而某个变量在file1文件中定义、在file2和file3文件中使用,那么在文件file2与file3就需要使用extern声明来建立该变量与其定义之间的联系。

“定义”表示创建变量或分配存储单元,而“声明”指的是说明变量的性质,但并不分配存储单元。

#include <stdio.h>

#define MAXLINE 1000

int max;
char line[MAXLINE];
char longest[MAXLINE];

int getline1(void);
void copy(void);

int main()
{
    int len;
    extern int max; // 在函数内部通过extern关键字声明外部变量
    char longest[]; // 由于变量在函数调用前定义,因此可以省略extern

    max = 0;
    while ((len = getline1()) > 0)
    {
        if (len > max)
        {
            max = len;
            copy();
        }
    }
    if (max > 0)
        printf("%s", longest);
    return 0;
}

int getline1(void)
{
    int c, i;
    extern char line[];

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

void copy(void)
{
    int i;
    extern char line[], longest[];

    i = 0;
    while ((longest[i] = line[i]) != '\0')
        ++i;
}

第2章 类型、运算符与表达式

变量和常量是程序处理的两种基本数据对象。声明语句说明变量的名字及类型,也可以指定变量的初值。运算符指定将要进行的操作。表达式则把变量与常量组合起来生成新的值。对象的类型决定该对象可取值的集合以及可以对该对象执行的操作。

所有整型int包括signed(带符号)和unsigned(无符号)两种形式,且可以表示无符号常量与十六进制字符常量。浮点运算可以以单精度进行,还可以使用更高的精度的long double类型。字符串常量可以在编译时连接。ANSI C还支持枚举类型。对象可以声明为const(常量)类型,表明其值不能修改。

2.1 变量名

变量名是由字母和数字组成的序列,其第一个字符必须为字母。下划线“_”被看做字母,通常用于命名较长的变量名,以提高可读性。在传统的C语言用法中,变量名使用小写字母,符号常量名全部使用大写字母。

局部变量一般使用较短的变量名,外部变量使用较长的名字。

2.2 数据类型及长度

C语言基本数据类型:

  • char 字符型,占用一个字节,可以存放本地字符集中的一个字符
  • int 整型,通常反映了所用机器中整数的最自然长度
  • float 单精度浮点型
  • double 双精度浮点型

此外,还可以在这些基本数据类型的前面加上一些限定符。short与long两个限定符用于限定整型:

short int sh;
long int counter;

在上述这种类型的声明中,关键字int可以省略。

short与long两个限定符的引入可以提供满足实际需要的不同长度的整型数,int通常代表特定机器中整数的自然长度。short类型通常为16位,long类型通常为32位,int类型可以为16位或32位。

类型限定符signed与unsigned可用于限定char类型或任何整型。unsigned类型的数总是正值或0,并遵循算法模2n2^n2n定律,其中n是该类型占用的位数。例如,如果char对象占用8位,那么unsigned char类型变量的取值范围为0~255,而signed char类型变量的取值范围为-128~127。不带限定符的char类型对象是否带符号取决于具体机器。

2.3 常量

int类型的常量类似于1 2 3 4。long类型的常量以字母l或L结尾,如1 2 3 4 5 6 7 8 9L。如果一个整数太大以至于无法用int类型表示,也将被当做long类型处理。无符号常量以字母u或U结尾。后缀ul或UL表明是unsigned long类型。

浮点数常量中包含一个小数点(如123.4)或一个指数(如1e-2),也可以两者都有。没有后缀的浮点数常量为double类型。后缀f或F表示float类型,而后缀l或L则表示long double类型。

整数除了用十进制表示外,还可以用八进制或十六进制表示。带前缀0的整型常量表示为八进制形式。前缀0x或0X表示为十六进制形式。

一个字符常量就是一个整数,书写时将一个字符括在单引号中,如'x'。字符在机器字符集中的数值就是字符常量的值。字符常量一般用来与其他字符进行比较,但也可以像其他整数一样参与数值运算。

某些字符可以通过转义字符序列(例如,换行符\n)表示为字符和字符串常量。转义字符序列看起来像两个字符,但只表示一个字符。

ANSI C语言中的全部转义字符如下所示:

  • \a 响铃符
  • \b 回退符
  • \f 换页符
  • \n 换行符
  • \r 回车符
  • \t 横向制表符
  • \v 纵向制表符
  • \\ 反斜杠
  • \? 问号
  • \' 单引号
  • \" 双引号
  • \ooo 八进制数
  • \xhh 十六进制数

常量表达式是仅仅包含常量的表达式。这种表达式在编译时求值,而不在运行时求值。它可以出现在常量可以出现的任何位置。例如:

#define MAXLINE 1000
char line[MAXLINE+1];

字符串常量也叫字符串字面值,是用双引号括起来的0个或多个字符组成的字符序列。例如:"I am a string"""

双引号不是字符串的一部分,它只用于限定字符串。字符常量中使用的转义字符序列同样可以用在字符串中。

字符串常量就是字符数组,字符串的内部表示使用一个空字符'\0'作为串的结尾,因此,存储字符串的物理存储单元数比括在双引号中的字符数多一个。这种表示也说明C语言对字符串的长度没有限制,但程序必须扫描完整个字符串后才能确定字符串的长度。

/* strlen函数:返回s的长度 */
int strlen(char s[])
{
    int i = 0;

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

'x'"x"是不同的,前者是一个整数,其值是字母x在机器字符集中对应的数值(内部表示值);后者是一个包含一个字符(即字母x)以及一个结束符'\0'的字符数组。

枚举常量是另外一种类型的常量。枚举是一个常量整型值的列表,例如:

enum boolean 
{
  NO, // 0
  YES // 1
};

enum escapes
{
  BELL = '\a',
  BACKSPACE = '\b'
}

enum months
{
  JAN = 1,
  FEB,
  MAR,
  APR
}

2.4 声明

所有变量都必须先声明后使用,尽管某些变量可以通过上下文隐式地声明。一个声明指定一种变量类型,后面所带的变量表可以包含一个或多个该类型的变量。例如:

int lower, upper, step;
char c, line[1000];

还可以在声明的同时对变量进行初始化。

char esc = '\\';
int i = 0;
int limit = MAXLINE+1;
float eps = 1.0e-5;

任何变量的声明都可以使用const限定符限定,该限定符指定变量的值不能被修改。对数组而言,const限定符指定数组所有元素的值都不能被修改:

const double e = 2.71821821421;
const char msg[] = "warning: ";

const限定符也可配合参数使用,它表明函数不能修改数组元素的值:

int strlen(const char[]);

2.5 算术运算符

取模运算符%不能应用于float或double类型。

2.6 关系运算符与逻辑运算符

在关系表达式或逻辑表达式中,如果关系为真,则表达式的结果值为数组1;如果为假,则结果值为数值0;

2.7 类型转换

自动转换是指把“比较窄的”操作数转换成“比较宽的”操作数,并且不丢失信息的转换,例如,在计算表达式f+i时,将整型变量i的值自动转换为浮点型。

// 将字符串s转换为相应的整型数
int atoi(char s[])
{
    int i, n;

    n = 0;
    for (i = 0; s[i] >= '0' && s[i] <= '9'; ++i)
        n = 10 * n + (s[i] - '0'); // s[i] - '0' 会自动转换成数字值进行计算
    return n;
}

// 将字符转换为小写形式
int lower(int c)
{
    if (c >= 'A' && c <= 'Z')
        return c + 'a' - 'A';
    else
        return c;
}

C语言中,很多情况下会进行隐式的算术类型转换。如果二元运算符的两个操作数具有不同的类型,那么在进行运算之前先要把“较低”的类型提升为“较高”的类型。运算的结果为较高的类型。

赋值时也要进行类型转换。赋值运算符右边的值需要转换为左边变量的类型,左边变量的类型即赋值表达式结果的类型。当把较长的整数转换为较短的整数或char类型时,超出的高位部分将被丢弃。当把float类型转换为int类型时,小数部分将被截取掉。

由于函数调用的参数是表达式,所以在把参数传递给函数时也可能进行类型转换。在没有函数原型的情况下,char与short类型都将被转换为int类型,float类型将被转换为double类型。

最后,在任何表达式中都可以使用一个称为强制类型转换的一元运算符强制进行显式类型转换。(类型名)表达式

char s = 'a';
printf("将char强制转换为int %d\n", (int) s);

注意,强制类型转换只是生成一个指定类型的n的值,n本身的值并没有改变。

2.9 位运算符

C语言提供了6个位操作运算符。这些运算符只能作用域整型操作数,即只能作用域带符号或无符号的char、short、int与long类型:

  • & 按位与(AND)
  • | 按位或(OR)
  • ^ 按位异或(XOR)
  • << 左移
  • >> 右移
  • ~ 按位求反(一元运算符)

2.10 赋值运算符与表达式

#include <stdio.h>

/* 统计x中值为1的二进制数 */
int bitcount(unsigned x)
{
    int b;

    for (b = 0; x != 0; x >>= 1)
        if (x & 01)
            b++;
    return b;
}

int main(void)
{
    int test = 7;
    printf("%d\n", bitcount(test));
    printf("%d\n", 0b0101);
    return 0;
}

大多数二元运算符都有一个相应的赋值运算符。例如:+=-=*=等等。

2.11 条件表达式

三元运算符(“?:”)

expr1 ? expr2 : expr3

2.12 运算符优先级与求值次序

2022_11_24_IMG_7748

优先级从上往下逐行降低。

第3章 控制流

3.1 语句与程序块

在x=0、i++或printf(…)这样的表达式之后加上一个分号(😉,它们就变成了语句。例如:

x = 0;
i++;
printf(...);

在C语言中,分号是语句结束符。

用一对花括号{}把一组声明和语句括在一起就构成了一个复合语句(也叫程序块),复合语句在语法上等价于单条语句(在任何程序块中都可以声明变量)。

3.2 if-elseif-else语句

if-else语句用于条件判定。

if(表达式1)
  语句1
else if(表达式2)
  语句2
else
  语句3

该语句执行时,先计算表达式1的值,如果其值为真(即表达式的值为非0),则执行语句1。否则进入else if执行表达式2,如果其值为真执行语句2,如果其值为假(即表达式的值为0),则执行语句3。

建议在有if语句嵌套的情况下使用花括号。

3.4 switch语句

switch (表达式) {
  case 常量表达式: 语句序列
  case 常量表达式: 语句序列
  default: 语句序列
}

如果某个分支与表达式的值匹配,则从该分支开始执行。

#include <stdio.h>

int main(void)
{
    int c, i, nwhite, nother, ndigit[10];

    nwhite = nother = 0;
    for (i = 0; i < 10; i++)
        ndigit[i] = 0;
    while ((c = getchar()) != EOF)
    {
        switch (c)
        {
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
            ndigit[c - '0']++;
            break; // break语句将导致程序立即从switch语句中退出。
        case ' ':
        case '\n':
        case '\t':
            nwhite++;
            break;
        default:
            nother++;
            break;
        }
    }
    printf("digits =");
    for (i = 0; i < 10; i++)
    {
        printf(" %d", ndigit[i]);
    }
    printf(", white space = %d, other = %d\n", nwhite, nother);
    return 0;
}

3.5 while循环与for循环

while (表达式)
  语句
  
for(表达式1; 表达式2; 表达式;)
  语句

等价于

表达式1;
while(表达式2) {
  语句
  表达式2;
}

在设计程序时到底选用while循环语句还是for循环语句,主要取决于程序设计人员的个人偏好。如果没有初始化或重新初始化的操作,使用while循环语句自然一些。如果语句中需要执行简单的初始化和变量递增,使用for语句更合适一些。

逗号运算符“,”是C语言中优先级最低的运算符,在for语句中常会用到它。被逗号分隔的一对表达式将按照从左到右的顺序进行求值。

#include <string.h>

void reverse(char s[])
{
    int c, i, j;

    for (i = 0, j = strlen(s) - 1; i < j; i++, j--)
    {
        c = s[i];
        s[i] = s[j];
        s[j] = c;
    }
}

3.6 do-while循环

do {

} while(表达式)

3.7 break语句与continue语句

break语句用于从for、while与do-while等循环中提前退出。

int trim(char s[])
{
    int n;

    for (n = strlen(s) - 1; n >= 0; n--)
        if (s[n] != ' ' && s[n] != '\t' && s[n] != '\n')
            break;
    s[n+1] = '\0';
    return n;
}

continue语句用于使for、while或do-while语句开始下一次循环的执行。continue只能用于循环语句,不用于switch语句。

3.8 goto语句与标号

goto语句常用于终止程序在某些深度嵌套的结构中的处理过程,例如一次跳出两层或多层循环。

标号的命名同变量命名的形式相同,标号后面紧跟一个冒号。标号可以位于对应的goto语句所在函数的任何语句的前面;标号的作用域是整个函数。

for (i = 0; i < n; i++)
  for (j = 0; j < m; j++)
    if (a[i] == b[j])
      goto found;
		...
found:
	...

所有使用goto语句的程序都能改写成不带goto语句的程序。大多数情况下,使用goto语句的程序段比不使用goto语句的程序段要难以理解和维护。建议尽量少用goto语句。

第4章 函数与程序结构

函数可以把大的计算任务分解成若干个较小的任务,程序设计人员可以基于函数进一步构造程序,而不需要重新编写一些代码。

// 模拟grep,通过函数组织程序

#include <stdio.h>
#define MAXLINE 1000

int getline1(char line[], int max);
int strindex(char source[], char searchfor[]);

char pattern[] = "ould";

int main()
{
    char line[MAXLINE];
    int found = 0;

    while (getline1(line, MAXLINE) > 0)
        if (strindex(line, pattern) >= 0)
        {
            printf("%s", line);
            found++;
        }
    printf("found:%d\n", found);
    return found;
}

int getline1(char s[], int lim)
{
    int c, i;
    i = 0;
    while (--lim > 0 && (c = getchar()) != EOF && c != '\n')
        s[i++] = c;
    if (c == '\n')
        s[i++] = c;
    s[i] = '\0';
    return i;
}

int strindex(char s[], char t[])
{
    int i, j, k;

    for (i = 0; s[i] != '\0'; i++)
    {
        for (j = i, k = 0; t[k] != '\0' && s[j] == t[k]; j++, k++)
            ;
        if (k > 0 && t[k] == '\0')
            return i;
    }
    return -1;
}

函数的定义形式如下:

返回值类型 函数名(参数声明表)
{
  声明和语句
}

// 函数声明中的各构成部分都可以省略,例
dummy() {}

如果函数定义中省略了返回值类型,则默认为int类型。

程序可以看成是变量定义和函数定义的集合。函数之间的通信可以通过参数、函数返回值以及外部变量进行。函数在源文件中出现的次序可以是任意的。

return后面可以跟任何表达式:return 表达式;

在UNIX系统中,使用cc命令来编译c源文件,会生成目标文件.o,再编译目标文件.o生成可执行文件。

4.2 返回非整型值的函数

为了显式知道调用函数返回的类型,可以在调用函数中显示声明函数,例如:

double sum, atof(char[]); // 显示声明atof函数返回double类型

4.3 外部变量

C语言不允许在一个函数中定义其他函数,因此函数本身是“外部的”。

外部变量与函数具有下列性质:通过同一个名字引用的所有外部变量实际上都是引用同一个对象。外部变量的用途还表现在它们与内部变量相比具有更大的作用域和更长的生存期。

4.4 作用域规则

构成C语言程序的函数与外部变量可以分开进行编译。一个程序可以存放在几个文件中,原先已编译过的函数可以从库中进行加载。

变量声明用于说明变量的属性(主要是变量的类型),而变量定义除此以外还将引起存储器的分配。

int sp;
double val[MAXVAL];

上面这两条语句将定义外部变量sp与val,并为之分配存储单元,同时这两条语句还可以作为该源文件中其余部分的声明。

extern int sp;
extern double val[];

上面为源文件声明一个int类型的外部变量sp以及一个double数组类型的外部变量val,但这两个声明并没有建立变量或为它们分配存储单元。

在一个源程序的所有源文件中,一个外部变量只能在某个文件中定义一次,而其他文件可以通过extern声明来访问它。外部变量的定义中必须指定数组的长度,但extern声明则不一定要指定数组的长度。

4.5 头文件

IMG_7766

对于某些中等规模的程序,最好只用一个头文件存放程序中各部分共享的对象。较大的程序需要使用更多的头文件。

4.6 静态变量

static声明限定外部变量与函数,可以将其后声明的对象的作用域限定为被编译源文件的剩余部分。

// getch.c

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

#define BUFSIZE 100

// 外部文件将无法访问下面两个变量
static char buf[BUFSIZE];
static int bufp = 0;

int getch(void)
{
    return (bufp > 0) ? buf[--bufp] : getchar();
}

void ungetch(int c)
{
    if (bufp >= BUFSIZE)
        printf("ungetch: too many characters\n");
    else
        buf[bufp++] = c;
}

static也可用于声明内部变量。static类型的内部变量同自动变量一样,是某个特定函数的局部变量,只能在该函数中使用,但它与自动变量不同的是,不管其所在函数是否被调用,它一直存在,而不像自动变量那样,随着所在函数的被调用和退出而存在和消失。

4.7 寄存器变量

register声明告诉编译器,它所声明的变量在程序中使用频率较高。其思想是,将register变量放在机器的寄存器中,这样可以使程序更小、执行速度更快。

register声明只适用于自动变量以及函数的形式参数。

f(register unsigned m, register long n)
{
  register int i;
  ...
}

实际使用时,每个函数中只有很少的变量可以保存在寄存器中,且只允许某些类型的变量。

4.8 程序块结构

变量的声明(包括初始化)除了可以紧跟在函数开始的花括号之后,还可以紧跟在任何其他标识复合语句开始的左花括号之后。

if (n > 0) {
  int i; // 声明一个新的变量i
  for (i = 0; i < n; i++)
    ...
}

每次进入程序块时,在程序块内声明以及初始化的自动变量都将被初始化。静态变量只在第一次进入程序块时被初始化一次。

4.9 初始化

在不进行显示初始化的情况下,外部变量静态变量都将被初始化为0,而自动变量寄存器变量的初值则没有定义(即初值为无用的信息)。

对于外部变量静态变量来说,初始化表达式必须是常量表达式。对于自动变量寄存器变量来说,则在每次进入函数或程序块时都将初始化,初始化表达式可以不是常量表达式。

数组的初始化是在声明的后面紧跟一个初始化表达式列表,初始化表达式列表用花括号括起来,各初始化表达式之间通过逗号分隔。

int days[] = { 31, 28, 31, 30, 31, 30 };

当省略数组的长度时,编译器将把花括号中初始化表达式的个数作为数组的长度。

如果初始化表达式的个数比数组元素少,则对外部变量、静态变量和自动变量来说,没有初始化表达式的元素将被初始化为0。初始化表达式的个数不能比数组元素数多。不能一次将一个初始化表达式指定给多个数组元素,也不能跳过前面的数组元素而直接初始化后面的数组元素。

字符数组的初始化比较特殊:

char pattern[] = "ould";
// 等同于
char pattern[] = { 'o', 'u', 'l', 'd', '\0' };
// 数组的长度是5,加上字符串结束符'\0'

4.10 递归

C语言的函数可以递归调用,即函数可以直接或间接调用自身。

// 快速排序,一个递归的例子

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

    temp = v[i];
    v[i] = v[j];
    v[j] = temp;
}

void qsort(int v[], int left, int right)
{
    int i, last;
    void swap(int v[], int i, int j);

    if (left >= right)
        return;
    swap(v, left, (left + right) / 2);
    last = left;
    for (i = left + 1; i <= right; i++)
        if (v[i] < v[left])
            swap(v, ++last, i);
    swap(v, left, last);
    qsort(v, left, last - 1);
    qsort(v, last + 1, right);
}

递归并不节省存储器的开销,因为递归调用过程中必须在某个地方维护一个存储处理值的栈。递归的执行速度并不快,但递归代码比较紧凑,并且比相应的非递归代码更易于编写与理解。

4.11 C预处理器

C语言通过预处理器提供了一些语言功能。从概念上将,预处理器是编译过程中单独执行的第一个步骤。两个最常用的预处理器指令是#include指令(用于在编译期间把指定文件的内容包含进当前文件中)和#define指令(用于以任意字符序列替代一个标记)。

4.11.1 文件包含
#include "文件名"
// or
#include <文件名>

的行都将被替换为由文件名指定的文件的内容。如果文件名用"",则在源文件所在的位置查找该文件;如果在该位置没有找到文件或如果文件名用<>,则将根据相应的规则查找该文件。

4.11.2 宏替换
// 宏定义的形式
#define 名字 替换文本

这是一种最简单的宏替换——后续所有出现名字记号的地方都将被替换为替换文本。替换文本可以是任意字符串。

#define指令定义的名字的作用域从其定义点开始,到被编译的源文件的末尾处结束。

替换文件可以是任意的,例如:

#define forever for (;;) /* 无限循环 */

// 宏定义也可以带参数,对不同的宏调用使用不同的替换文本
#define max(A, B) ((A) > (B) ? (A) : (B))

使用#undef指令取消名字的宏定义,这样做可以保证后续的调用是函数调用,而不是宏调用:

#undef getchar

int getchar(void) { ... }

形式参数不能用带引号的字符串替换,但是,如果在替换文本中带参数名以#作为前缀,则结果将被扩展为由实际参数替换该参数的带引号的字符串。例:

#include <stdio.h>

#define dprint(expr) printf(#expr " = %g\n", expr)

int main()
{
    double x = 4.0;
    double y = 2.0;
  	// 调用将被扩展为printf("x/y" " = %g\n", x/y);
    // 打印 x/y = 2
    dprint(x/y);
    return 0;
}

预处理器运算符##为宏扩展提供了一种连接实际参数的手段。如果替换文本中的参数与##相邻,则该参数将被实际参数替换,##与前后的空白符将被删除,被对替换后的结果查询扫描。例:

// 宏调用paste(name, 1)的结果将被创建记号name1
#define paste(front, back) front ## back
4.11.3 条件包含

使用条件语句对预处理器本身进行控制,这种条件语句的值是在预处理执行的过程中进行计算。

#if语句对其中的常量整型表达式(其中不能包含sizeof、类型转换运算符或enum常量)进行求值,若该表达式的值不等于0,则包含其后的各行,直到遇到#endif#enif#else语句为止。在#if语句中可以使用表达式defined,该表达式的值遵循下列规则:当名字已经定义时,其值为1;否则,其值为0。

#if !defined(HDR)
#define HDR

/* hdr.h文件的内容放在这里 */

#endif

第一次包含头文件hdr.h时,将定义名字HDR;此后再次包含该头文件时,会发现该名字已经定义,这样讲直接跳转到endif处。类似的方式也可以用来避免多次重复包含同一文件。

C语言专门定义了两个预处理语句#ifdef#ifndef,它们用来测试某个名字是否已经定义。

#ifndef HDR
#define HDR

/* hdr.h文件的内容放在这里 */

#endif

第5章 指针与数组

指针是一种保存变量地址的变量。

5.1 指针与地址

通常的机器都有一系列连续编号或编址的存储单元,这些存储单元可以单个进行操纵,也可以以连续成组的方式操纵。通过情况下,机器的一个字节可以存放一个char类型的数据,两个相邻的字节存储单元可存储一个short(短整型)类型的数据,而4个相邻的字节存储单元可存储一个long类型的数据。指针是能够存放一个地址的一组存储单元(通常是2或4个字节)。

IMG_7772

上图中,假如c是char,并且p是指向c的指针。

一元运算符&可用于取一个对象的地址。地址运算符&只能应用于内存中的对象,即变量与数组元素。它不能作用于表达式、常量或register类型的变量。

// c的地址赋值给变量p,称p位“指向”c的指针。
p = &c;

一元运算符*是间接寻址或间接引用运算符。当它作用于指针时,将访问指针所指向的对象。

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]

对函数的声明:

// atof的参数是一个指向char类型的指针
double *dp, atof(char *);

指针只能指向某种特定类型的对象,也就是说,每个指针都必须指向某种特定的数据类型(一个例外是指向void类型的指针可以存放任何类型的指针,但它不能间接引用其自身)。

5.2 指针与函数参数

由于C语言是以传值的方式将参数传递给调用函数的,因此,被调用函数不能直接修改主调函数中变量的值。可以使用指针来实现修改。

void swap(int *px, int *py)
{
  int temp;
  
  temp = *px;
  *px = *py;
  *py = temp;
}

swap(&a, &b);

5.3 指针与数组

在C语言中,指针和数组之间的关系十分密切。通过数组下标所能完成的任何操作都可以通过指针来实现。一般来说,用指针编写的程序比用数组下标编写的程序执行速度快。

IMG_7774 IMG_7775

上图中

int *pa;
pa = &a[0];
IMG_7776

pa指向数组中的某个特定元素,那么,pa+1将指向下一个元素,pa+i将指向pa所指向数组元素之后的第i个元素。如果指针pa指向a[0],那么*(pa+1)引用的是数组元素a[1]的内容,pa+i是数组元素a[i]的地址。

下标和指针运算之间具有密切的对应关系。根据定义,数组类型的变量或表达式的值是该数组第0个元素的地址。执行pa=&a[0]后,pa和a具有相同的值。因为数组名所代表的就是该数组最开始的一个元素的地址,所以,该语句也可以写成pa=a

对数组元素a[i]的引用也可以写成*(a+1)这种形式。在计算数组元素a[i]的值时,C语言实际上先将其转换成*(a+i)的形式,然后再进行求值,因此在程序中这两种形式是等价的。简而言之,一个通过数组和下标实现的表达式可等价地通过指针和偏移量实现。

数组名和指针之间有一个不同之处。指针是一个变量,因此语句pa=a和pa++都是合法的,但数组名不是变量,因此a=pa和a++形式的语句是非法的。

当把数组名传递给一个函数时,实际上传递的是该数组第一个元素的地址。

int strlen(char *s)
{
    int n;

    for (n = 0; *s != '\0'; s++)
        n++;

    return n;
}

strlen("hello world");
strlen(array) // 字符数组array有100个元素
strlen(ptr) // ptr是一个指向char类型对象的指针

在函数定义中,char s[]char *s是等价的。我们通常更习惯使用后一种形式,因为它比前者更直观地表明了该参数是一个指针。如果将数组名传递给函数,函数可以根据情况判定是按照数组处理还是按照指针处理,随后根据相应的方式操作该参数。

也可以将指向子数组起始位置的指针传递给函数,比如f(&a[2])f(a+2)都将把a[2]的子数组的地址传递给函数f。

5.4 地址算术运算

C语言中的地址算术运算方法是一致且有规律的,将指针、数组和地址的算术运算集成在一起是该语言的一大优点。

下面将一个不完善的存储分配程序。

#define ALLOCSIZE 10000

static char allocbuf[ALLOCSIZE];

static char *allocp = allocbuf;

char *alloc(int n) // 返回指向n个字符的指针
{
    if (allocbuf + ALLOCSIZE - allocbuf >= n) // 有足够的空闲空间
    {
        allocp += n;
        return allocp - n; // 分配前的指针p
    }
    else // 空闲空间不够
    {
        return 0; // C语言中,0永远不是有效的数据地址
    }
}

void afree(char *p) // 释放p指向的存储区
{
    if (p >= allocbuf && p < allocbuf + ALLOCSIZE)
        allocp = p;
}
IMG_7779

一般情况下,同其他类型的变量一样,指针也可以初始化。通常,对指针有意义的初始化值只能是0或者是表示地址的表达式,对后者来说,表达式所代表的地址必须是在此前已定义的具有适当类型的数据的地址。

函数返回值0可用来表示发生了异常事件

指针与整数之间不能相互转换,但0是唯一的例外:常量0可以赋值给指针,指针也可以和常量0进行比较。程序中常用符号常量NULL代替常量0,这样便于更清晰地说明常量0是指针的一个特殊值。符号常量NULL定义在标准头文件<stddef.h>中。

有效的指针运算包括相同类型指针之间的赋值运算;指针同整数之间的加法与减法运算;指向相同数组中元素的两个指针间的减法或比较运算;将指针赋值为0或指针与0之间的比较运算。

5.5 字符指针与函数

字符串常量最常见的用法也许是作为函数参数,例如:

printf("hello, world\n");

当类似于这样的一个字符串出现在程序中时,实际上是通过字符指针访问该字符串的。在上述语句中,printf接受的是一个指向字符数组第一个字符的指针。也就是说,字符串常量可通过一个指向其第一个元素的指针访问。

char amessage[] = "now is the time"; // 定义一个数组
char *pmessage = "now is the time"; // 定义一个指针

上述声明中,amessage是一个仅仅足以存放初始化字符串以及空字符’\0’的一维数组。数组中的单个字符可以进行修改,但amessage始终指向同一个存储位置。另一方面,pmessage是一个指针,其初值指向一个字符串常量,之后它可以被修改以指向其他地址,但如果试图修改字符串的内容,结果是没有定义的。

IMG_7837
// 模拟标准库<string.h>的strcpy函数

void strcpy1(char *s, char *t)
{
    while ((*s++ = *t++) != '\0')
        ;
}

void strcpy2(char *s, char *t)
{
    while (*s++ = *t++)
        ;
}
// 模拟标准库<string.h>的strcmp函数

// 用数组实现
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];
}

// 用指针实现
int strcmp2(char *s, char *t)
{
    for (; *s == *t; s++, t++)
        if (*s != '\0')
            return 0;
    return *s - *t;
}

5.6 指针数组以及指向指针的指针

由于指针本身也是变量,所以它们也可以像其他变量一样存储在数组中。

// 数组的每个元素是一个指向字符类型对象的指针。
char *lineptr[MAXLENGS]

5.7 多维数组

// 这里声明为char类型,是为了说明在char类型的变量中存放较小的非字符整数也是合法的
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}}

// 将某月某日的日期表示形式转换为某年中第几天的表示形式
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;
}

// 将某年中第几天的日期表示形式转换为某月某日的表达形式
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 = 1; yearday > daytab[leap][i]; i++)
        yearday -= daytab[leap][i];
    *pmonth = i;
    *pday = yearday;
}

如果要将daytab传递给函数,函数应该写成f(int daytab[2][13]),也可以写成f(int daytab[][13]),因为数组的行数无关紧要,所以还可以写成f(int (*daytab)[13])。一般来说,除数组的第一维(下标)可以不指定大小外,其余各维都必须明确指定大小。

5.8 指针数组的初始化

// 返回n各月份的名字
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];
}

5.9 指针与多维数组

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

指针数组的一个重要优点在于,数组的每一行长度可以不同。

IMG_7841

5.10 命令行参数

在支持C语言的环境中,可以在程序开始执行时将命令行参数传递给程序。调用主函数main时,它有两个参数。第一个参数(argc,参数计数)的值表示运行程序时命令行中参数的数目;第二个参数(argv,参数向量)是一个指向字符串数组的指针,其中每个字符串对应一个参数。

按照C语言的约定,argv[0]的值是启动该程序的程序名,因此argc的值至少为1。如果argc的值为1,则说明程序名后面没有命令行参数。

#include <stdio.h>

// 版本1
int 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;
}

// 版本2
int main(int argc, char *argv[])
{
    while (--argc > 0)
        printf("%s%s", *++argv, (argc > 1) ? " " : "");
    printf("\n");
}

5.11 指向函数的指针

在C语言中,函数本身不是变量,但可以定义指向函数的指针。这种类型的指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值等等。

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

#define MAXLINES 5000
char *lineptr[MAXLINES];

int readlines(char *lineptr[], int nlines);
void writelines(char *lineptr[], int nlines);

// 第四个参数,它表明comp是一个指向函数的指针,该函数具有两个void *类型的参数,其返回值类型为int
void qsort(void *lineptr[], int left, int right, int (*comp)(void *, void *));
int numcmp(char *, char *);

int main(int argc, char *argv[])
{
    int nlines;
    int numeric = 0;

    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;
    }
}

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++)
        // *comp指向函数,然后通过传参的方式调用函数
        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);
}

在调用函数qsort的语句中,strcmp和numcmp是函数的地址。因为它们是函数,所以前面不需要加上取地址运算符&,同样的原因,数组名前面也不需要&运算符。

5.12 复杂声明

C语言的声明容易让人混淆,原因在于,C语言的声明不能从左至右阅读,而且使用了太多的圆括号。

int *f(); // f是一个函数,返回一个指向int类型的指针

int (*pf)(); // pf是一个指向函数的指针,该函数返回一个int类型的对象

第6章 结构

结构是一个或多个变量的集合,这些变量可能为不同的类型,为了处理的方便而将这些变量组织在一个名字之下。

结构可以拷贝、赋值、传递给函数,函数也可以返回结构类型的返回值。

6.1 结构的基本知识

struct point {
    int x;
    int y;
};

关键字struct引入结构声明。结构声明由包含在花括号内的一系列声明组成。关键字struct后面的名字的可选的,称为结构标记(这里指point)。结构标记用于为结构命名,在定义之后,结构标记就代表花括号内的声明,可以用它作为声明的简写形式。

结构中定义的变量称为成员。结构成员、结构标记和普通变量可以采用相同的名字,它们之间不会冲突,因为通过上下文分析总可以对它们进行区分。

struct声明定义了一种数据类型。在标志结构成员表结束的右花括号之后可以跟一个变量表,这与其他基本类型的变量声明是相同的。例如

struct { ... } x, y, z;

**如果结构声明的后面不带变量表,则不需要为它分配存储空间,它仅仅描述了一个结构的模板或轮廓。**如果结构声明中带有标记,则以后定义结构实例时便可以使用该标记定义。例如:

struct point pt;

结构的初始化可以在定义的后面使用初值表进行。初值表中同每个成员对应的初值必须是常量表达式。例如:

struct point maxpt = { 320, 200 };

在表达式中,可以通过结构名.成员引用某个特定结构中的成员。

结构可以嵌套。例如:

struct rect {
  struct point pt1;
  struct point pt2;
}

6.2 结构与函数

结构的合法操作只有几种:作为一个整体复制和赋值,通过&运算符取地址,访问其成员。其中,复制和赋值包含向函数传递参数以及从函数返回值。结构之间不可以进行比较。

结构类型的参数和其他类型的参数一样,都是通过值传递的。

// 将两个点相加
struct point addpoint(struct point p1, struct point p2)
// p1和p2都是通过值传递
{
    p1.x += p2.x;
    p1.y += p2.y;
    return p1;
}

如果传递给函数的结构很大,使用指针方式的效率通常比复制整个结构的效率要高。例:

struct point *pp;

pp = &origin;
printf("origin is (%d,%d)\n", (*pp).x, (*pp).y);

结构指针的使用频率非常高,为了使用方法,C语言提供了p->结构成员的简写方式(假设p是一个指向结构的指针)。例:

printf("origin is (%d,%d)\n", pp->x, pp->y);

在所有运算符中,下面4个运算符的优先级最高:结构运算符.->、用于函数调用的()以及用于下标的[]

6.3 结构数组

struct key {
  char *word;
  int count;
};

struct key keytab[NKEYS];

// 初始化
struct key keytab[] = {
    {"auto", 0},
    {"break", 0},
    {"case", 0},
    // ...
    {"void", 0}
};

结构数组的长度=数组项的长度 × 项数。

C语言提供了一个编译时(compile-time)一元运算符sizeof,它可用来计算任一对象的长度,表达式sizeof 对象sizeof(类型名)将返回一个整型值,它等于指定对象或类型占用的存储空间字节数(严格来说,sizeof的返回值是无符号整型值,其类型为size_t,该类型在头文件<stddef.h>中定义)。其中,对象可以是变量、数组或结构;类型可以是基本类型,如int、double,也可以是派生类型,如结构类型或指针类型。

// 可以用来计算数组项的长度
#define NKEYS (sizeof keytab / sizeof(struct key))

条件编译语句#if中不能使用sizeof,因为预处理器不对类型名进行分析。但预处理器并不计算#define语句中的表达式。因此在#define中使用sizeof是合法的。

6.4 指向结构的指针

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;
}

6.5 自引用结构

struct tnode {
  char *word;
  int count;
  struct tnode *left;
  struct tnode *right;
}

自引用结构的一种变体:两个结构相互引用。例:

struct t {
  ...
  struct s *p; // p指向一个s结构
};
struct s {
  ...
  struct t *q; // q指向一个t结构
};

malloc函数的作用是分配内存,它返回一个void类型的指针。对于任何执行严格类型检查的语言来说,像malloc这样的函数的类型声明总是很令人头疼的问题。在C语言中,一种合适的方法是将malloc的返回值声明为一个指向void类型的指针,然后再显式地将该指针强制转换为所需类型。例:

#include <stdlib.h>

// 创建一个tnode
struct tnode *talloc(void)
{
    return (struct tnode *) malloc(sizeof(struct tnode));
}

6.6 表查找

考虑到#define语句的查找。表查找算法采用的是散列查找方法——将输入的名字转换为一个小的非负整数,该整数随后将作为一个指针数组的下标。数组的每个元素指向某个链表的表头,链表的各个块用于描述具有该散列值的名字。如果没有名字散列到该值,则数组元素的值为NULL。

IMG_7863

链表中的每个块都是一个结构,它包含一个指向名字的指针、一个指向替换文本的指针以及一个指向该链表后继块的指针。如果指向链表后继块的指针为NULL,则表明链表结束。

#define HASHSIZE 101

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

// 为字符串s生成散列值
unsigned hash(char *s)
{
    unsigned hashval;

    for (hashval = 0; *s != '\0'; s++)
        hashval = *s + 31 * hashval;
    return hashval % HASHSIZE;
}

// 在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; // 找到s
    return NULL;
}

// 将(name, defn)加入到hashtab中
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);
    if ((np->defn = strdup(defn)) == NULL)
        return NULL;
    return np;
}

6.7 类型定义(typedef)

C语言提供了一个称为typedef的功能,它用来建立新的数据类型名。例如,声明

typedef int Length;

typedef char *String;

将Length定义为与int具有同等意义的名字。类型Length可用于类型声明、类型转换等,它与类型int完全相同,例如:

Length len, maxlen;
Length *lengths[];

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

typedef声明并没有创建一个新声明,它只是为某个已存在的类型增加了一个新的名称而已。typedef声明也没有增加任何新的语义:通过这种方式声明的变量与通过普通声明的变量具有完全相同的属性。

6.8 联合

联合是可以(在不同时刻)保存不同类型和长度的对象的变量,编译器负责跟踪对象的长度和对齐要求。联合提供了一种方式,以在单块存储区中管理不同类型的数据,而不需要在程序中嵌入任何机器有关的信息。

联合的目的是一个变量可以合法地保存多种数据类型中任何一种类型的对象。例:

// 这些类型中的任何一种类型的对象都可赋值给u,且可使用在随后的表达式中,但必须保证是一致的:读取的类型必须是最近一次存入的类型。
union u_tag {
  int ival;
  float fval;
  char *sval;
} u;

可以通过联合名.成员联合指针->成员访问联合中的成员。它与访问结构的方式相同。

实际上,联合就是一个结构,它的所有成员相对于基地址的偏移量都为0,此结构空间要大到足够容纳最“宽”的成员,并且,其对齐方式要适合联合中所有类型的成员。对联合允许的操作与对结构允许的操作相同:作为一个整体单元进行赋值、复制、取地址及访问其中一个成员。

6.9 位字段

C语言提供了直接定义和访问一个字中的位字段的能力,而不需要通过按位逻辑运算符。位字段(bit-field),简称字段,是“字”中相邻位的集合。例:

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

上面的代码中,三个位字段is_keyword、is_extern、is_static的长度为1位,即只能存储0或1。字段不是数组,并且没有地址,因此对它们不能使用&运算符。

第7章 输入与输出

7.1 标准输入/输出

文本流由一系列行组成,每一行的结尾是一个换行符。

最简单的输入机制是使用getchar函数从标准输入中一次读取一个字符:

int getchar(void)

getchar函数在每次调用时返回下一个输入字符。若遇到文件结尾,则返回EOF。符号常量EOF在头文件<stdio.h>中定义,其值一般为-1。程序应该使用EOF来测试文件是否结束,这样才能保证程序同EOF的特定值无关。

在许多环境中,可以使用符号<来实现输入重定向,它将把键盘输入替换位文件输入:如:

// 假设prog程序使用了函数getchar
prog <infile

函数int putchar(int)用于输出数据。通常情况下,可以使用>输出文件名的格式将输出重定向到某个文件中。

使用输入/输出库函数的每个源程序文件必须在引用这些函数之前包含下列语句:

#include <stdio.h>

当文件名用一对尖括号<和>括起来时,预处理器将在由具体实现定义的有关位置中查找指定的文件。

7.2 格式化输出——printf函数

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

函数printf在输出格式format的控制下,将其参数进行转换与格式化,并在标准输出设备上打印出来。它的返回值为打印的字符数。

IMG_7871

在转换说明中,宽度或精度可以用星号*表示,这时,宽度或精度的值通过转换下一参数(必须为int类型)来计算。例如:printf("%.*s", max, s);

7.3 变长参数表

本节介绍如何以可移植的方式编写可处理变长参数表的函数。

函数的声明形式如下:

void miniprintf(char *fmt, ...)

标准头文件<stdarg.h>中包含一组宏定义,它们对如何遍历参数表进行了定义。

// 简化版printf

#include <stdio.h>
#include <stdarg.h>

void miniprintf(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.4 格式化输入——scanf函数

输入函数scanf对应于输出函数printf,它在与后者相反的方向上提供同样的转换功能。声明形式如下:

int scanf(char *format, ...)

scanf函数从标准输入中读取字符序列,按照format中的格式说明对字符序列进行解释,并把结果保存到其余的参数中。scanf函数的返回值是匹配到参数的个数。其他所有参数都必须是指针,用于指定经格式转换后的相应输入保存的位置。

格式串通常都包含转换说明,用于控制输入的转换。格式串可能包含下列部分:

  • 空格或制表符,在处理过程中将被忽略,
  • 普通字符(不包括%),用于匹配输入流中下一个非空白符字符。
  • 转换说明,依次由一个%、一个可选的赋值禁止字符*、一个可选的数值(指定最大字段宽度)、一个可选的h、l或L字符(指定目标对象的宽度)以及一个转换字符组成。
image-20221212152725895
//  简易计算器

#include <stdio.h>

int main(void)
{
    double sum, v;

    sum = 0;
    while (scanf("%lf", &v) == 1)
        printf("\t%.2f\n", sum += v);
    return 0;
}

7.5 文件访问

标准输入和标准输出是操作系统自动提供给程序访问的。

在读写一个文件之前,必须通过库函数fopen打开该文件。fopen返回一个随后可以用于文件读写操作的指针。该指针称为文件指针,它指向一个包含文件信息的结构,这些信息包括:缓冲区的位置、缓冲区中当前字符的位置、文件的读或写状态、是否出错或是否已经到达文件结尾等等。<stdio.h>头文件中已定义了一个包含这些信息的结构FILE

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

在程序中,可以这样调用fopen函数:

// 第一个参数是文件名,第二个参数是访问模式,允许的模式包括读r、写w和追加a
fp = fopen(name, mode);

文件被打开后,需要考虑采用哪些方法对文件进行读写。有多种方法,其中getc和putc函数最为简单。getc从文件中返回下一个字符:int getc(FILE *fp)。如果到达文件尾或出现错误,该函数将返回EOF。putc是一个输出函数:int putc(int c, FILE *fp),该函数将字符c写入到fp指向的文件中,并返回写入的字符。如果发生错误则返回EOF。

启动一个C语言程序时,操作系统环境负责打开3个文件,并将这3个文件的指针提供给该程序。这3个文件分别是标准输入、标准输出和标准错误,相应的文件指针分别是stdin、stdout和stderr,它们在<stdio.h>中声明。在大多数环境中,stdin指向键盘,而stdout和stderr指向显示器。

fscanf和fprintf与scanf和printf的区别在于它们的第一个参数是一个指向所要读取的文件的指针,第二个参数是格式串。

// cat函数

#include <stdio.h>

int 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); // 断开由open函数建立的文件指针和外部名之间的连接,并释放文件指针以供其他文件使用
            }
    return 0;
}

// 将文件ifp复制到文件ofp
void filecopy(FILE *ifp, FILE *ofp)
{
    int c;
    while ((c = getc(ifp)) != EOF)
        putc(c, ofp);
}

当程序正常终止时,程序会自动为每个打开的文件调用fclose函数。

7.6 错误处理——stderr和exit

// cat函数,处理错误

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

int main(int argc, char *argv[])
{
    FILE *fp;
    void filecopy(FILE *, FILE *);
    char *prog = argv[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); // 调用时将会终止程序的执行,任何调用该程序的进程都可以获取exit的参数值,exit为每个已打开的输出文件调用fclose函数,以将缓冲区中的所有输出写到相应的文件中
            }
            else
            {
                filecopy(fp, stdout);
                fclose(fp); // 断开由open函数建立的文件指针和外部名之间的连接,并释放文件指针以供其他文件使用
            }
    // 如果stdout出现错误,ferror将返回一个非0值
    if (ferror(stdout)) {
        fprintf(stderr, "%s: error writing stdout\n", prog);
        exit(2);
    }
    exit(0);
}

// 将文件ifp复制到文件ofp
void filecopy(FILE *ifp, FILE *ofp)
{
    int c;
    while ((c = getc(ifp)) != EOF)
        putc(c, ofp);
}

在主程序中,语句return expr等价于exit(expr)。使用exit函数的优点是它可以从其他函数中调用。

7.7 行输入和行输出

标准库提供了一个输入函数fgets,它和前面几章的getline类似。

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

fgets函数从fp指向的文件中读取下一个输入行,并将它存放在字符数组line中,它最多可读取maxline-1个字符。读取的行将以'\0'结尾保存到数组中。

输出函数fputs将一个字符串(不需要包含换行符)写入到一个文件中:

int fputs(char *line, FILE *fp)

如果发生错误,该函数返回EOF,否则返回一个非负值。

库函数gets和puts与fgets和fputs函数类似,但它们是对stdin和stdout进行操作。

char *fgets(char *s, int n, FILE *iop)
{
    register int c;
    register char *cs;

    cs = s;
    while (--n > 0 && (c = getc(iop)) != EOF)
        if ((*cs++ = c) == '\n')
            break;
    *cs = '\0';
    return (c == EOF && cs == s) ? NULL : s;
}

int fputs(char *s, FILE *iop)
{
    int c;

    while(c = *s++)
        putc(c, iop);
    return ferror(iop) ? EOF : 2; // 这里用非负值,具体值取决于具体场景
}

// 用fgets很容易实现getline
int getline(char *line, int max)
{
    if (fgets(line, max, stdin) == NULL)
        return 0;
    else
        return strlen(line);
}

7.8 其他函数

7.8.1 字符串操作函数
image-20221213093751948

s、t为char *类型,c与n为int类型。

7.8.2 字符类别测试和转换函数

头文件<ctype.h>中定义了一些用于字符测试和转换的函数。

image-20221213094103864

c是一个可表示unsigned char类型或EOF的int对象。

7.8.4 命令执行函数

函数system(char *s)执行包含在字符串s中的命令。s的内容在很大程度上与所用的操作系统有关。

system("date");
7.8.5 存储管理函数

函数malloccalloc用于动态地分配存储块。malloc的声明如下:

void *malloc(size_t n)

calloc的声明为:

void *calloc(size_t n, size_t size)

根据请求的对象类型,malloc或calloc函数返回的指针满足正确的对齐要求。下面例子进行了类型转换:

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

free(p)函数释放p指向的存储空间,其中,p是此前通过调用malloc或calloc函数得到的指针。存储空间的释放顺序没有什么限制,但是,如果释放一个不是通过调用malloc或calloc函数得到的指针所指向的存储空间,将是一个很严重的错误。

使用已经释放的存储空间同样是错误的。

7.8.6 数学函数
image-20221213095457113
7.8.7 随机数发生器函数

函数rand()生成介于0和RAND_MAX之间的伪随机整数序列。RAND_MAX常量在<stdlib.h>中定义。下面是一个生成大于等于0但小于1的随机浮点数的方法:

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

第8章 UNIX系统接口

系统调用实际上是操作系统内的函数,它们可以被用户程序调用。

8.1 文件描述符

在UNIX操作系统中,所有的外围设备(包括键盘和显示器)都被看作是文件系统中的文件,因此,所有的输入/输出都要通过读文件或写文件完成。也就是说,通过一个单一的接口就可以处理外围设备和程序之间的所有通信。

任何时候对文件的输入/输出都是通过文件描述符标识文件,而不是通过文件名标识文件。系统负责维护已打开文件的所有信息,用户程序只能通过文件描述符引用文件。

8.2 低级I/O——read和write

输入和输出是通过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);

每个调用返回实际传输的字节数。

8.3 open、creat、close和unlink

除了默认的标准输入、标准输出和标准错误文件外,其他文件都必须在读或写之前显示地打开。

  • open系统调用用于打开一个文件,返回一个文件描述符。
  • creat系统调用创建新文件或覆盖已有的旧文件。

8.7 实例——存储分配程序

malloc并不是从一个在编译时就确定的固定大小的数组中分配存储空间,而是在需要时向操作系统申请空间。因为程序中的某些地方可能不通过malloc调用申请空间,所以,malloc管理的空间不一定是连续的。空闲存储空间以空闲块链表的方式组织,每个块包含一个长度、一个指向下一块的指针以及一个指向自身存储空间的指针。

IMG_7893

当有申请请求时,malloc将扫描空闲块链表,直到找到一个足够大的块为止。它寻找满足条件的最小块,如果该块恰好与请求的大小相符合,则将它从链表中移走并返回给用户。如果该块太大,则将它分成两部分:大小合适的块返回给用户,剩下的部分留在空闲块链表中。如果找不到一个足够大的块,则向操作系统申请一个大块并加入到空闲块链表中。

释放过程也是首先搜索空闲块链表,以找到可以插入被释放块的合适位置。如果与被释放块相邻的任一边是一个空闲块,则将这两个块合成一个更大的块,这样存储空间不会有太多的碎片。因为空闲块链表是以地址的递增顺序链接在一起的,所以很容易判断相邻的块是否空闲。

在malloc函数中,请求的长度(以字符为单位)将被舍入,以保证它是头部大小的整数倍。实际分配的块将多包含一个单元,用于头部本身。实际分配的块的大小将被记录在头部的size字段中。malloc函数返回的指针将指向空闲空间,而不是块的头部。

IMG_7894

其中size字段是必须的,因为由malloc函数控制的块不一定是连续的,这样就不可能通过指针算术计算其大小。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值