《C 程序设计语言》笔记

第 1 章 导言

入门

  • 用双引号括起来的字符序列成为 字符串字符串常量,如 "hello, world\n" 就是一个字符串。

变量与算术表达式

  • 声明 用于说明变量的属性,它由一个类型名和一个变量表组成,例如 int fahr, celsius; int lower, upper, step;

函数

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

参数——传值调用

  • 在 C 语言中,所有函数参数都是「通过值」传递的。也就是说,传递给被调用函数的参数值存放在临时变量中,而不是存放在原来的变量中。

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

字符数组

  • 当在 C 语言程序中出现类似于 "hello\n" 的字符串常量时,它将以字符数组的形式存储,数组的各元素分别存储字符串的各个字符,并以 '\0' 标记字符串的结束。

+-+-+-+-+-+--+--+
|h|e|l|l|o|\n|\0|
+-+-+-+-+-+--+--+

外部变量与作用域

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

  • 外部变量必须定义在所有函数之外,且只能定义一次,定义后编译程序将为它分配存储单元。在每个需要访问外部变量的函数中,必须声明相应的外部变量,此时说明其类型。声明时可以用 extern 语句显示声明,也可以通过上下文隐式声明。

  • 在 ANSI C 中,如果要声明空参数表,则必须使用关键字 void 进行显示声明。

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

运算符优先级与求值次序

  • C 语言没有指定同一运算符中多个操作数的计算顺序&&||?:, 运算符除外)。例如,在形如 x = f() + g(); 的语句中,f() 可以在 g() 之前计算,也可以在 g() 之后计算。为了保证特定的计算顺序,可以把中间结果保存在临时变量中。

  • C 语言也没有指定函数各参数的求值顺序。因此,下列语句 printf("%d %d\n", ++n, power(2, n)); 在不同的编译器中可能会产生不同的结果,这取决于 n 的自增运算在 power 调用之前还是之后执行。

第 3 章 控制流

// TODO

第 4 章 函数与程序结构

返回非整型值的函数

  • 如果没有函数原型,则函数将在第一次出现时的表达式中被隐式声明,例如 sum += atof(line),如果先前没有声明过的一个名字出现在某个表达式中,并且其后紧跟一个左圆括号,那么上下文就会认为该名字是一个函数名字,该函数的返回值将被假定为 int 类型,但上下文并不对其参数做任何假设。并且,如果函数声明中不包含参数,例如:double atof();,那么编译程序也不会对函数 atof 的参数做任何假设,并会关闭所有的参数检查。

外部变量

  • 默认情况下,外部变量与函数具有下列性质:通过同一个名字引用的所有外部变量(即使这种引用来自单独编译的不同函数)实际上都是引用同一个对象(标准中把这一性质称为「外部链接」)。

  • 外部变量是永久存在的,它们的值在一次函数调用到下一次函数调用之间保持不变。

作用域

  • 外部变量或函数的作用域从声明它的地方开始,到其所在的(待编译的)文件的末尾结束。

  • 如果要在外部变量的定义之前使用该变量,或者外部变量的定义与变量的使用不在同一个源文件中,则必须在相应的变量声明中强制性地使用关键字 extern

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

  • 外部变量的初始化只能出现在其定义中

静态变量

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

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

初始化

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

  • 对于外部变量与静态变量来说,初始化表达式必须是常量表达式,且只初始化一次(从概念上讲是在程序开始执行前初始化)。

  • 对于自动变量与寄存器变量来说,初始化表达式可以不是常量表达式:表达式中可以包含任意在此表达式之前已经定义的值,包括函数调用。

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

int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  • 当省略数组的长度时,编译器将把花括号中初始化表达式的个数作为数组的长度

  • 如果初始化表达式的个数比数组元素少,则对外部变量、静态变量和自动变量来说,没有初始化表达式的元素将被初始化为 0。如果初始化表达式的个数比数组元素多,则是错误的。

  • 字符数组的初始化比较特殊:可以用一个字符串来代替用花括号括起来并用逗号分隔的初始化表达式序列

char pattern[] = "ould";
// 同下面的声明是等价的
char pattern[] = { 'o', 'u', 'l', 'd', '\0' };

递归

  • 函数递归调用自身时,每次调用会得到一个与以前的自动变量集合不同的新的自动变量集合。

C 预处理器

  • 从概念上讲,预处理器是编译过程中单独执行的第一个步骤。

  • 在源文件中,任何形如 #include "文件名"#include <文件名> 的行都将被替换为由文件名指定的文件的内容。如果文件名用引号引起来,则在源文件所在位置查找该文件;如果在该位置没有找到文件,或者如果文件名用尖括号括起来,则将根据相应的规则查找该文件,这个规则同具体的实现有关。

  • 宏定义的形式如下:#define 名字 替换文本,这是一种最简单的宏替换——后续所有出现「名字」记号的地方都将被替换为「替换文本」。

  • 可以把一个较长的宏定义分成若干行,这时需要在待续的行末尾加上一个反斜杠符 \

  • 替换只对记号进行,对括在引号中的字符串不起作用

  • 可以用 #undef 指令取消名字的宏定义。

  • 如果在替换文本中参数名以 # 作为前缀,则结果将被扩展为由实际参数替换该参数的带引号的字符串

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

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

第 5 章 指针与数组

指针与地址

  • ANSI C 使用类型 void *(指向 void 的指针)代替 char * 作为通用指针的类型。

  • 一元运算符 & 可用于取一个对象的地址,因此,下列语句 p = &c; 将把 c 的地址赋值给变量 p,我们称 p 为「指向」c 的指针。地址运算符 & 只能应用于内存中的对象,即变量与数组元素。它不能作用于表达式、常量或 register 类型的变量。

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

       +--------------+
       ^              v
----+-+-+-+------+-+-+-+-+----
....| |p| |......| | |c| |....
----+-+-+-+------+-+-+-+-+----

指针与函数参数

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

  • 指针参数使得被调用函数能够访问和修改主调函数中的对象的值。

指针与数组

  • 根据定义,数组类型的变量或表达式的值是该数组第 0 个元素的地址。执行赋值语句 pa = &a[0]; 后,pa 和 a 具有相同的值。因为数组名所代表的就是该数组最开始的一个元素的地址。所以赋值语句 pa = &a[0]; 也可以写成下列形式 pa = a;。对数组元素 a[i] 的引用也可以写成 *(a + i) 这种形式。

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

地址算术运算

  • 如果 p 是一个指向数组中某个元素的指针,那么 p++ 将对 p 进行自增运算并指向下一个元素,而 p+i 将对 p 进行加 i 的增量运算,使其指向指针 p 当前所指向的元素之后的第 i 个元素。

  • 有效的指针运算包括:

    • 相同类型指针之间的赋值运算;

    • 指针同整数之间的加法或减法运算;

    • 指向相同数组中元素的两个指针间的减法或比较运算;

    • 将指针赋值为 0 或指针与 0 之间的比较运算;

    • 其它所有形式的指针运算都是非法的。

字符指针与函数

  • C 语言中没有提供将整个字符串作为一个整体进行处理的运算符。

char message1[] = "now is the time";

message1:
 v
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+
|n|o|w| |i|s| |t|h|e| |t|i|m|e|\0|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+

char *message2 = "now is the time";

message2:
 v
+-+    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+
| |--->|n|o|w| |i|s| |t|h|e| |t|i|m|e|\0|
+-+    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+
  • message1 是一个仅仅足以存放初始化字符串以及空字符 \0 的一维数组。数组中的单个字符可以进行修改,但 message1 始终指向同一个存储位置。

  • message2 是一个指针,其初值指向一个字符串常量,之后它可以被修改以指向其它地址,但如果试图修改字符串的内容,结果是没有定义的

多维数组

  • 在 C 语言中,二维数组实际上是一种特殊的一维数组,它的每个元素也是一个一维数组。因此,数组的下标应该写成 daytab[i][j] // [行][列],而不能写成 daytab[i, j]

  • 如果将二维数组作为参数传递给函数,那么在函数的参数声明中必须指明数组的列数。

指针与多维数组

int a[10][20];
int *b[10];
  • 从语法角度讲,a[3][4]b[3][4] 都是对一个 int 对象的合法引用。但 a 是一个真正的二维数组,它分配了 200 个 int 类型长度的存储空间,并且通过常规的矩阵下标计算公式 20 * row + col(其中 row 表示行,col 表示列)计算得到元素 a[row][col] 的位置。但是,对 b 来说,该定义仅仅分配了 10 个指针,并且 没有对它们初始化,它们的初始化必须以显式的方式进行,比如静态初始化或通过代码初始化。

  • 指针数组的一个重要优点在于,数组的每一行长度可以不同,也就是说,b 的每个元素不必都指向一个具有 20 个元素的向量,某些元素可以指向具有 2 个元素的向量,某些元素可以指向具有 50 个元素的向量,而某些元素可以不指向任何向量。

  • 指针数组最频繁的用处是存放具有不同长度的字符串。

char *name1[] = { "Illegal month", "Jan", "Feb", "Mar" };

name1:
+-+    +-+-+-+-+-+-+-+-+-+-+-+-+-+--+
| |--->|I|l|l|e|g|a|l| |m|o|n|t|h|\0|
| |    +-+-+-+-+-+-+-+-+-+-+-+-+-+--+
+-+    +-+-+-+--+
| |--->|J|a|n|\0|
| |    +-+-+-+--+
+-+    +-+-+-+--+
| |--->|F|e|b|\0|
| |    +-+-+-+--+
+-+    +-+-+-+--+
| |--->|M|a|r|\0|
+-+    +-+-+-+--+

char name2[][15]={ "Illegal month", "Jan", "Feb", "Mar" };

name2:
0                            15                             30                             45                             60
+-+-+-+-+-+-+-+-+-+-+-+-+-+--+-+-+-+-+--+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+-+-+-+-+-+-+-+-+-+-+-+
|I|l|l|e|g|a|l| |m|o|n|t|h|\0| |J|a|n|\0| | | | | | | | | | | |F|e|b|\0| | | | | | | | | | | |M|e|b|\0| | | | | | | | | | | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+--+-+-+-+-+--+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+-+-+-+-+-+-+-+-+-+-+-+

命令行参数

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

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

  • ANSI 标准要求 argv[argc] 的值必须为一空指针。

指向函数的指针

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

  • 任何类型的指针都可以转换为 void * 类型,并且在将它转换回原来的类型时不会丢失信息

第 6 章 结构

结构的基本知识

  • ANSI 标准在结构方面最主要的变化是定义了结构的赋值操作——结构可以拷贝、复制、传递给函数,函数也可以返回结构类型的返回值。

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

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

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

结构和函数

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

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

  • 结构指针的使用频率非常高,为了使用方便,C 语言提供了另一种简写方式。假定 p 是一个指向结构的指针,则可以用 p->结构成员 这种形式引用相应的结构成员。

结构数组

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

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

指向结构的指针

  • 千万不要认为结构的长度等于各成员长度的和。因为不同的对象有不同的对齐要求,所以,结构中可能会出现未命名的「空穴(hole)」。例如 char 类型占用一个字节,int 类型占用 4 个字节,则下列结构可能需要 8 个字节的存储空间,而不是 5 个字节。使用 sizeof 运算符可以返回正确的对象长度。

struct {
	char c;
    int i;
}

自引用结构

  • 一个包含其自身实例的结构是非法的,但是一个包含其自身实例指针的结构是合法的。

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

类型定义(typedef)

  • C 语言提供了一个称为 typedef 的功能,它用来建立新的数据类型名,例如声明 typedef int Length 将 Length 定义为与 int 具有同等意义的名称。

  • 从任何意义上讲,typedef 声明并没有创建一个新类型,它只是为某个已存在的类型增加了一个新的名称而已。typedef 声明也没有增加任何新的语义:通过这种方式声明的变量与通过普通声明方式声明的变量具有完全相同的属性。实际上,typedef 类似于 #define 语句,但由于 typedef 是由编译器解释的,因此它的文本替换功能要超过预处理器。

  • 除了表达方式更简洁之外,使用 typedef 还有另外两个重要原因:

    • 可以使程序参数化,以提高程序的可移植性。如果 typedef 声明的数据类型同机器有关,那么,当程序移植到其它机器上时,只需改变 typedef 类型定义就可以了。

    • 为程序提供更好的说明性。

联合(union)

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

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

  • 对联合读取的类型必须是最近一次存入的类型,程序员负责跟踪当前保存在联合中的类型。如果保存的类型与读取的类型不一致,其结果取决于具体的实现。

  • 可以通过下列语法访问联合中的成员:联合名.成员联合指针->成员,它与访问结构的方式相同。

  • 实际上,联合就是一个结构,它的所有成员相对于基地址的偏移量都为 0,此结构空间要大到足够容纳最宽的成员,并且,其对齐方式要适合于联合中所有类型的成员。

  • 对联合允许的操作与对结构允许的操作相同:作为一个整体单元进行赋值、复制、取地址及访问其中一个成员。

  • 联合只能用其第一个成员类型的值进行初始化

位字段(bit-field)

  • C 语言提供了直接定义和访问一个「字(word)」中的「位字段」的能力,而不需要通过按位逻辑运算符。位字段,或简称字段,是字中相邻位的集合。字是单个的存储单元,它同具体的实现有关。

  • 定义一个变量 flags,它包含 3 个一位的字段。冒号后的数字表示字段的宽度(用二进制位数表示)。字段被声明为 unsigned int 类型,以保证它们是无符号的。

struct {
	unsigned int is_keyword : 1;
    unsigned int is_extern  : 1;
    unsigned int is_static  : 1;
} flags;
  • 字段的所有属性几乎都同具体的实现有关。字段是否能覆盖字边界由具体的实现定义。字段可以不命名,无名字段(只有一个冒号和宽度)起填充作用。特殊宽度 0 可以用来强制在下一个字边界上对齐。

最后更新于