《C 语言程序设计:现代方法》复习查漏笔记

版权声明:本文为 gfson 原创文章,转载请注明出处。
注:作者水平有限,文中如有不恰当之处,请予以指正,万分感谢。


第2章 C 语言基本概念

2.1. 字符串字面量(String Literal)

字符串字面量是用一对双引号包围的一系列字符。

2.2. 把换行符加入到字符串中

  • 错误方式
    printf("hello
    World\n");
  • 正确方式
    printf("hello "
    "World");

根据 C 语言标准,当两条或者更多条字符串字面量相连时(仅用空白字符分割),编译器必须把它们合并成单独一条字符串。这条规则允许把字符串分割放在两行或更多行中。

2.3. 编译器是完全移走注释还是用空格替换掉注释?

根据标准 C,编译器必须用一个空格符替换每条注释语句。
a/**/b = 0 相当于 a b = 0

2.4. 在注释中嵌套一个新的注释是否合法?

在标准 C 中是不合法的,如果需要注释掉一段包含注释的代码,可以使用如下方法:

    #if 0
    printf("hello World\n");
    #endif

这种方式经常称为「条件屏蔽」。

2.5. C 程序中使用 // 作为注释的开头,如下所示,是否合法?

// This is a comment.

在标准 C 中是不合法的,// 注释是 C++ 的方式,有一些 C 编译器也支持,但是也有一部分编译器不支持,为了保持程序的可移植性,因此应该尽量避免使用 //


第3章 格式化的输入/输出

3.1. 转换说明(Conversion specification)

转换说明以 % 开头,转化说明用来表示填充位置的占位符。

3.2. printf 中转换说明的格式

转化说明的通用格式为:

%m.pX 或者 %-m.pX

对于这个格式的解释如下:

  • m 和 p 都是整形常量,而 X 是字母。
  • m 和 p 都是可选项。
    • 如果省略 m,小数点要保留,例如 %.2f
    • 如果省略 p,小数点也要一起省略,例如 %10f
  • m 表示的是最小字段宽度(minimum field width),指定了要显示的最小字符数量。
    • 如果要打印的数值比 m 个字符少,那么值在字段内是右对齐的(换句话说,在数值前面放置额外的空格)。
    • 如果要打印的数值比 m 个字符多,那么字段宽度会自动扩展为需要的尺寸,而不会丢失数字。
    • 在 m 前放上一个负号,会发生左对齐。
    • 举例如下:
      printf("%4d\n",1);
      printf("%4d\n",11);
      printf("%4d\n",111);
      printf("%4d\n",1111);
    
      printf("---------------\n");
    
      printf("%4d\n",12345);
    
      printf("---------------\n");
    
      printf("%-4d\n",1);
      printf("%-4d\n",11);
      printf("%-4d\n",111);
      printf("%-4d\n",1111);
    
  • p 表示的是精度(precision),p 的含义依赖于转换说明符(conversion specifier) X 的值,X 表明需要对数值进行哪种转换。常见的转换说明符有:
    • d —— 表示十进制的整数。当 x 为 d 时,p 表示可以显示的数字的最少个数(如果需要,就在数前加上额外的零),如果忽略掉 p,则默认它的值为 1。
    • e —— 表示指数(科学计数法)形式的浮点数。当 x 为 e 时,p 表示小数点后应该出现的数字的个数(默认为 6),如果 p 为 0,则不显示小数点。
    • f —— 表示「定点十进制」形式的浮点数,没有指数。p 的含义与在说明符 e 时一样。
    • g —— 将 double 值转化为 f 形式或者 e 形式,形式的选择根据数的大小决定。仅当数值的指数部分小于
      -4,或者指数部分大于或等于精度时,会选择 e 形式显示。当 x 为 g 时,p 表示可以显示的有效数字的最大数量(默认为 6)。与 f 不同,g 的转换将不显示尾随零。
    • 举例如下:
      printf("%.3d\n",1);
      printf("%.5d\n",1);
      printf("%.6d\n",1);
    
      printf("--------------------\n");
    
      printf("%e\n",12.1);
      printf("%.2e\n",12.1);
      printf("%.0e\n",12.1);
    
      printf("--------------------\n");
    
      printf("%f\n",12.1);
      printf("%.2f\n",12.1);
      printf("%.0f\n",12.1);
    
      printf("--------------------\n");
    
      printf("%g\n",12.120000);
      printf("%g\n",12.12345678);
      printf("%.2g\n",12.1);
      printf("%.0g\n",12.1);
    

3.3. %i 和 %d 有什么区别?

在 printf 中,两者没有区别。在 scanf 中,%d 只能和十进制匹配,而 %i 可以匹配八进制、十进制或者十六进制。如果用户意外将 0 放在数字的开头处,那么用 %i 代替 %d 可能有意外的结果。由于这是一个陷阱,所以坚持使用 %d。

3.4. printf 如何显示字符 %?

printf 格式串中,两个连续的 % 将显示一个 %,如下所示:

    printf("%%");

第4章 表达式

4.1. 运算符 /% 注意的问题

  • 运算符 / 通过丢掉分数部分的方法截取结果。因此,1/2 的结果是 0 而不是 0.5。
  • 运算符 % 要求整数操作数,如果两个操作数中有一个不是整数,无法编译通过。
  • 当运算符 /% 用于负的操作数时,其结果与实现有关。
    • -9/7 的结果既可以是 -1 也可以是 -2。
    • -9%7 的结果既可以是 2 也可以是 -2。

4.2. 由实现定义(implementation-defined)

  • 由实现定义是一个术语,出现频率很高。
  • C 语言故意漏掉了语言未定义部分,并认为这部分会由「实现」来具体定义。
  • 所谓实现,是指软件在特定平台上编译、链接和执行。
  • C 语言为了达到高效率,需要与硬件行为匹配。当 -9 除以 7 时,一些机器产生的结果可能是 -1,而另一些机器的结果可能是 -2。C 标准简单的反映了这一现实。
  • 最好避免编写与实现行为相关的程序。

4.3. 赋值运算符「=」

  • 许多语言中,赋值是语句,但是 C 语言中,赋值是运算符,换句话说,赋值操作产生结果
  • 赋值表达式 v = e 产生的结果就是赋值运算后 v 的值。
  • 运算符 = 是右结合的,i = j = k = 0 相当与 (i = (j = (k = 0)))
  • 由于结果发生了类型转换,串联赋值运算的最终结果不是期望的结果,如下所示:
    int i;
    float f;

    f= i =33.6;

    printf("i=%d,f=%f",i,f);

4.4. 左值

  • 左值(lvalue)表示储存在计算机内存中的对象,而不是常量或计算结果。
  • 变量是左值,诸如 10 或者 2*i 这样的表达式不是左值。
  • 赋值运算符要求它左边的操作数必须是左值,以下表达式是不合法的,编译不通过:
    • 12 = i;
    • i + j = 0;
    • -i = j;

4.5. 子表达式的求值顺序

C 语言没有定义子表达式的求值顺序(除了含有逻辑与运算符及逻辑或运算符、条件运算符以及逗号运算符的子表达式)。因此,在表达式 (a + b) * (c - d) 中,无法确定子表达式 (a + b) 是否在子表达式 (c - d) 之前求值。

  • 这样的规定隐含着陷阱,如下所示:
      a = 5;
      c = (b = a + 2) - (a = 1);
    
    • 如果先计算 b = a + 2,则 b = 7,c = 6。
    • 如果先计算 a = 1,则 b = 3,c = 2。

为了避免此问题,最好不要编写依赖子表达式计算顺序的程序,一个好的建议是:不在字表达式中使用赋值运算符,如下所示:

  a = 5;
  b = a + 2;
  a = 1;
  c = b - a;

4.6. v += e 一定等价与 v = v + e 么?

不一定,如果 v 有副作用,则两者不想等。

  • 计算 v += e 只是求一次 v 的值,而计算 v = v + e 需要求两次 v 的值。任何副作用都能导致两次求 v 的值不同。如下所示:
    a [i++] += 2;  // i 自增一次
    a [i++] = a [i++] + 2;  // i 自增两次
    

4.7. ++ 和 -- 是否可以处理 float 型变量?

可以,自增和自减可以用于所有数值类型,但是很少使用它们处理 float 类型变量。如下所示:

    float f = 1.3;
    printf("%f",++f);

4.8. 表达式的副作用(side effect)

表达式有两种功能,每个表达式都产生一个值(value),同时可能包含副作用(side effect)。副作用是指改变了某些变量的值。如下所示:

  20          // 这个表达式的值是 20,它没有副作用,因为它没有改变任何变量的值。
  x=5         // 这个表达式的值是 5,它有一个副作用,因为它改变了变量 x 的值。
  x=y++       // 这个表达示有两个副作用,因为改变了两个变量的值。
  x=x++       // 这个表达式也有两个副作用,因为变量 x 的值发生了两次改变。

4.9. 顺序点(sequence point)

表达式求值规则的核心在于顺序点。

  • 顺序点的意思是在一系列步骤中的一个「结算」的点,C 语言要求这一时刻的求值和副作用全部完成,才能进入下面的部分。
  • C 标准规定代码执行过程中的某些时刻是 Sequence Point,当到达一个 Sequence Point 时,在此之前的 Side Effect 必须全部作用完毕,在此之后的 Side Effect 必须一个都没发。至于两个 Sequence Point 之间的多个 Side Effect 哪个先发生哪个后发生则没有规定,编译器可以任意选择各 Side Effect 的作用顺序。
  • C 语言中常见顺序点的位置有:
    • 分号 ;
    • 未重载的逗号运算符的左操作数赋值之后,即 ; 处。
    • 未重载的 || 运算符的左操作数赋值之后,即 || 处。
    • 未重载的 && 运算符的左操作数赋值之后,即 && 处。
    • 三元运算符 ? : 的左操作数赋值之后,即 ? 处。
    • 在函数所有参数赋值之后但在函数第一条语句执行之前。
    • 在函数返回值已拷贝给调用者之后但在该函数之外的代码执行之前。
    • 在每一个完整的变量声明处有一个顺序点,例如 int i, j; 中逗号和分号处分别有一个顺序点。
    • for 循环控制条件中的两个分号处各有一个顺序点。

第5章 选择语句

5.1. 表达式 i < j < k 是否合法?

此表达式是合法的,相当于 (i < j) < k,首先比较 i 是否小于 k,然后用比较产生的结果 1 或 0 来和 k 比较。

5.2. 如果 i 是 int 型,f 是 float 型,则条件表达式 i > 0 ? i : f 是哪一种类型的值?

当 int 和 float 混合在一个表达式中时,表达式类型为 float 类型。如果 i > 0 为真,那么变量 i 转换为 float 型后的值就是表达式的值。


第7章 基本类型

7.1. 读 / 写整数

  • 读写无符号数时,使用 u、o、x 代替 d。
    • u:表示十进制。
    • o : 表示八进制。
    • x:表示十六进制。
  • 读写短整型时,在 d、u、o、x 前面加上 h。
  • 读写长整型时,在 d、u、o、x 前面加上 l。

7.2. 转义字符(numeric escape)

在 C 语言中有三种转义字符,它们是:一般转义字符、八进制转义字符和十六进制转义字符。

  • 一般转义字符:这种转义字符,虽然在形式上由两个字符组成,但只代表一个字符。常用的有:
    • \a \n \t \v \b \r \f \\ \’ \"
  • 八进制转义字符:
    • 它是由反斜杠 \ 和随后的 1~3 个八进制数字构成的字符序列。例如,\60\101\141 分别表示字符 0Aa。因为字符 0Aa 的 ASCII 码的八进制值分别为 60、101 和 141。字符集中的所有字符都可以用八进制转义字符表示。如果你愿意,可以在八进制数字前面加上一个 0 来表示八进制转移字符。
  • 十六进制转义字符:
    • 它是由反斜杠 / 和字母 x(或 X)及随后的 1~2 个十六进制数字构成的字符序列。例如,\x30\x41\X61 分别表示字符 0Aa。因为字符 0Aa 的 ASCII 码的十六进制值分别为
      0x30、0x41 和 0x61。可见,字符集中的所有字符都可以用十六进制转义字符表示。
  • 由上可知,使用八进制转义字符和十六进制转义字符,不仅可以表示控制字符,而且也可以表示可显示字符。但由于不同的计算机系统上采用的字符集可能不同,因此,为了能使所编写的程序可以方便地移植到其他的计算机系统上运行,程序中应少用这种形式的转义字符。

7.3. 读字符的两种惯用法

while (getchar() != '\n') /* skips rest of line */
    ;
while ((ch = getchar()) == ' ') /* skips blanks */
    ;

7.4. sizeof 运算符

sizeof 运算符返回的是无符号整数,所以最安全的办法是把其结果转化为 unsigned long 类型,然后用 %lu 显示。

printf("Size of int: %lu\n", (unsigned long)sizeof(int));

7.5. 为什么使用 %lf 读取 double 的值,而用 %f 进行显示?

  • 一方面,函数 scanf 和 printf 有可变长度的参数列表,当调用带有可变长度参数列表的函数时,编译器会安排 float 自动转换为 double,其结果是 printf 无法分辨 float 和 double。所以在 printf 中 %f 既可以表示 float 又可以表示 double。
  • 另一方面,scanf 是通过指针指向变量的。%f 告诉 scanf 函数在所传地址上存储一个 float 类型的值,而 %lf 告诉 scanf 函数在所传地址上存储一个 double 类型的值。这里两者的区别很重要,如果给出了错误的转换,那么 scanf 可能存储错误的字节数量。

第11章 指针

11.1 指针总是和地址一样么?

通常是,但不总是。在一些计算机上,指针可能是偏移量,而不完全是地址

char near *p;      /*定义一个字符型“近”指针*/
char far *p;       /*定义一个字符型“远”指针*/
char huge *p;      /*定义一个字符型“巨”指针*/

近指针、远指针、巨指针是段寻址的 16bit 处理器的产物(如果处理器是 16 位的,但是不采用段寻址的话,也不存在近指针、远指针、巨指针的概念),当前普通 PC 所使用的 32bit 处理器(80386 以上)一般运行在保护模式下的,指针都是 32 位的,可平滑地址,已经不分远、近指针了。但是在嵌入式系统领域下,8086 的处理器仍然有比较广泛的市场,如 AMD 公司的 AM186ED、AM186ER 等处理器,开发这些系统的程序时,我们还是有必要弄清楚指针的寻址范围。

  • 近指针
    • 近指针是只能访问本段、只包含本段偏移的、位宽为16位的指针。
  • 远指针
    • 远指针是能访问非本段、包含段偏移和段地址的、位宽为32位的指针。
  • 巨指针
    • 和远指针一样,巨指针也是 32 位的指针,指针也表示为 16 位段:16 位偏移,也可以寻址任何地址。它和远指针的区别在于进行了规格化处理。远指针没有规格化,可能存在两个远指针实际指向同一个物理地址,但是它们的段地址和偏移地址不一样,如 23B0:0004 和 23A1:00F4 都指向同一个物理地址 23B04!巨指针通过特定的例程保证:每次操作完成后其偏移量均小于 10h,即只有最低 4 位有数值,其余数值都被进位到段地址上去了,这样就可以避免 Far 指针在 64K 边界时出乎意料的回绕的行为。

11.2. const int * p、int * const p、const int * const p

  • const int * p
    • 保护 p 指向的对象。
  • int * const p
    • 保护 p 本身。
  • const int * const p
    • 同时保护 p 和它指向的对象。

第12章 指针和数组

12.1. * 运算符和 ++ 运算符的组合

表达式 含义
*p++ 或 *(p++) 自增前表达式的值是 *p,然后自增 p
(*p)++ 自增前表达式的值是 *p,然后自增 *p
*++p 或 *(++p) 先自增 p,自增后表达式的值是 *p
++*p 或 ++(*p) 先自增 *p,自增后表达式的值是 *p

12.2. i[a] 和 a[i] 是一样的?

是的。
对于编译器而言,i[a] 等同与 *(i+a),a[i] 等同与 *(a+i),所以两者相同。

12.3. *a 和 a[]

  • 在变量声明中,指针和数组是截然不同的两种类型。
  • 在形式参数的声明中,两者是一样的,在实践中,*a 比 a[] 更通用,建议使用 *a。

第13章 字符串

13.1. 字符串字面量的赋值

char *p;
p = "abc";

这个赋值操作不是复制 "abc" 中的字符,而是使 p 指向字符串的第一个字符

13.2. 如何存储字符串字面量

  • 从本质上讲,C 语言将字符串字面量作为字符数组来处理,为长度为 n 的字符串字面量分配 n+1 的内存空间,最后一个空间用来存储空字符 \0
  • 既然字符串字面量作为数组来储存,那么编译器会将他看作 char* 类型的指针

13.3. 对指针添加下标

char ch
ch = "abc"[1];

ch 的新值将是 b。
如下,将 0 - 15 的数转化成等价的十六进制:

char digit_to_hex_char (int digit)
{
          return "0123456789ABCDEF"[digit];
}

13.4. 允许改变字符串字面量中的字符

char *p = "abc";
*p = 'b'; /* string literal is now "bbc" */

不推荐这么做,这么做的结果是未定义的,对于一些编译器可能会导致程序异常。

  • 针对 "abc" 来说,会在 stack 分配 sizeof(char *) 字节的空间给指针 p,然后将 p 的值修改为 "abc" 的地址,而这段地址一般位于只读数据段中。
  • 在现代操作系统中,可以将一段内存空间设置为读写数据、只读数据等等多种属性,一般编译器会将 "abc" 字面量放到像 ".rodata" 这样的只读数据段中,修改只读段会触发 CPU 的保护机制 (#GP) 从而导致操作系统将程序干掉。

13.5. 字符数组和字符指针

char ch[] = "hello world";
char *ch = "hello world";

两者区别如下:

  • 在声明为数组时,就像任意元素一样,可以修改存储在 ch 中的字符。在声明为指针时,ch 指向字符串字面量,而修改字符串字面量会导致程序异常。
  • 在声明为数组时,ch 是数组名。在声明为指针时,ch 是变量,这个变量可以在程序执行期间指向其他字符串。

13.6. printf 和 puts 函数写字符串

  • 转换说明 %s 允许 printf 写字符串,printf 会逐个写字符串的字符,直到遇到空字符串为止(如果空字符串丢失,则会越过字符串末尾继续写,直到在内存某个地方找到空字符串为止)。
    char p[] = "abc";
    printf("p=%s\n",p);
    
  • 转换说明 %m.ps%-m.ps 显示字符串
    • m 表示在大小为 m 的域内显示字符串,对于超过 m 个字符的字符串,显示完整字符串;对于少于 m 个字符的字符串,在域内右对齐。为了强制左对齐,在 m 前加一个负号。
    • p 代表要显示的字符串的前 p 个字符。
    • %m.ps 表示字符串的前 p 个字符在大小为 m 的域内显示
  • puts 的使用方式如下,str 就是需要显示的字符串。在写完字符串后,puts 总会添加一个额外的换行符。
    puts(str);
    

13.7. scanf 和 gets 函数读字符串

  • 转换说明 %s 允许 scanf 函数读入字符串,如下所示。不需要在 str 前加运算符 &,因为 str 是数组名,编译器会自动把它当作指针来处理。
    scanf("%s", str);
    
    • 调用时,scanf 会跳过空白字符,然后读入字符,并且把读入的字符存储到 str 中,直到遇到空白字符为止。scanf 始终会在字符串末尾存储一个空字符 \0
    • 用 scanf 函数读入字符串永远不会包括空白字符。因此,scanf 通常不会读入一整行输入。
  • gets 函数可以读入一整行输入。类似 scanf,gets 函数把读到的字符存储到数组中,然后存储一个空字符。
    gets(str);
    
  • 两者区别:
    • gets 函数不会在开始读字符之前跳过空白字符(scanf 函数会跳过)。
    • gets 函数会持续读入直到找到换行符为止(scanf 会在任意空白符处停止)。
    • gets 会忽略换行符,不会把它存储到数组里,而是用空字符代替换行符。
  • scanf 和 gets 函数都无法检测何时填满数据,会有数组越界的可能。
  • 使用 %ns 代替 %s 可以使 scanf 更安全,n 代表可以存储的最大字符数量。
  • 由于 gets 和 puts 比 scanf 和 printf 简单,因此通常运行也更快。

13.8. 自定义逐个字符读字符串函数

  • 在开始存储之前,不跳过空白字符。
  • 在第一个换行符处停止读取(不存储换行符)。
  • 忽略额外的字符。
#include <stdio.h>
#include <stdlib.h>

int read_line(char[] ,int);

int main(void)
{
    char str[10];
    int n = 10;

    read_line(str, n);
    printf("--- end ---");
    return 0;
}

int read_line(char ch[], int n)
{
    char tmp_str;
    int i = 0;

    while((tmp_str = getchar()) != '\n')
    {
        if(i<n)
        {
            ch[i++] = tmp_str;
        }
    }
    ch[i] = '\0';
    printf("message is:%s\n", ch);
    return i;
}

13.9. 字符串处理函数

C 语言字符串库 string.h 的几个常见函数:

  • strcpy 函数(字符串复制)
    char* strcpy (char* s1, const char* s2);
    
    • 把字符串 s2 赋值给 s1 直到(并且包括)s2 中遇到的一个空字符为止。
    • 返回 s1。
    • 如果 s2 长度大于 s1,那么会越过 s1 数组的边界继续复制,直到遇到空字符为止,会覆盖未知内存,结果无法预测。
  • strcat 函数(字符串拼接)
    char* strcat (char* s1, const char* s2);
    
    • 把字符串 s2 的内容追加到 s1 的末尾
    • 返回 s1。
    • 如果 s1 长度不够 s2 的追加,导致 s2 覆盖 s1 数组末尾后面的内存,结果是不可预测的。
  • strcmp 函数(字符串比较)
    int strcmp (const char* s1, const char* s2)
    
    • 比较 s1 和 s2,根据 s1 是否小于、等于、大于 s2,会返回小于、等于、大于 0 的值。
    • strcmp 利用字典顺序进行字符串比较,比较规则如下:
      • abc 小于 bcdabc 小于 abdabc 小于 abcd
      • 比较两个字符串时,strcmp 会查看表示字符的数字码。以 ASCII 字符集为例:
        • 所有大写字母(65 ~ 90)都小于小写字母(97 ~ 122)。
        • 数字(48 ~ 57)小于字母。
        • 空格符(32)小于所有打印字符。
  • strlen 函数(求字符串长度)
    size_t strlen (const char* s1)
    
    • 返回 s1 中第一个空字符串前的字符个数,但不包括第一个空字符串。
    • 当数组作为函数实际参数时,strlen 不会测量数组的长度,而是返回数组中的字符串长度。

13.10. 字符串惯用法

  • strlen 的简单实现
size_t strlen(const char * str) {
    const char *cp =  str;
    while (*cp++ )
         ;
    return (cp - str - 1 );
}
  • strcat 的简单实现
char* strcat ( char * dst , const char * src )
{
    char * cp = dst;
    while( *cp )
        cp++; /* find end of dst */
    while( *cp++ = *src++ ) ; /* Copy src to end of dst */
        return( dst ); /* return dst */
}

13.11. 存储字符串数组的两种方式

  • 二维字符数组
char planets[][8] = {"Mercury","Venus","Earth","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto"};

这种方式浪费空间,如下所示:

  • 字符串指针数组
char *planets[] = {"Mercury","Venus","Earth","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto"};

推荐这种方式,如下所示:

13.12. read_line 检测读入字符是否失败

  • 增加对 EOF 的判断。
int read_line(char ch[], int n)
{
    char tmp_str;
    int i = 0;

    while((tmp_str = getchar()) != '\n' && ch != EOF)
    {
        if(i<n)
        {
            ch[i++] = tmp_str;
        }
    }
    ch[i] = '\0';
    printf("message is:%s\n", ch);
    return i;
}

第14章 预处理器

14.1. # 运算符

宏定义中使用 # 运算符可以将宏的参数(调用宏时传递过来的实参)转化为字符串字面量,如下所示:

#define PRINT_INT(x) printf(#x " = %d\n", x)

宏展开后的结果是:

printf("x" " = %d\n", x)  等价于 printf("x = %d\n", x) 

14.2. ## 运算符

宏定义中使用 ## 运算符可以将两个记号粘在一起,成为一个记号。如果其中有宏参数,则会在形式参数被实际参数替换后进行粘合。如下所示:

#define MK_ID(n) i##n
int MK_ID(1), MK_ID(2), MK_ID(3);

上述宏展开后结果为:

int i1, i2, i3;

14.3. 宏定义的作用范围

一个宏定义的作用范围通常到出现这个宏的文件末尾。

  • 由于宏是由预处理器处理的,他们不遵从通常的范围规则。
  • 一个定义在函数内的宏,并不仅在函数内起作用,而是作用的文件末尾。

14.4. 宏定义加圆括号

  • 如果宏的替换列表有运算符,始终要将替换列表放在圆括号中。
  • 如果宏有参数,参数每次在替换列表出现的时候,必须放在圆括号中。

14.5. 预定义宏

宏名字 宏类型 宏作用
__LINE__ 当前行的行数 整型常量
__FILE__ 当前源文件的名字 字符串字面量
__DATE__ 编译的日期(Mmm dd yyyy) 字符串字面量
__TIME__ 编译的时间(hh:mm:ss) 字符串字面量
__STDC__ 如果编译器接收标准 C,那么值为 1 整型常量

第15章 编写大规模程序

15.1. 共享宏定义和类型定义

15.2. 共享函数原型

  • 文件 stack.c 中包含 stack.h,以便编译器检查 stack.h 中的函数原型是否与 stack.c 中的函数定义相匹配。
  • 文件 calc.c 中包含 stack.h,以便编译器检查每个函数的返回类型,以及形式参数的数量和类型是否匹配。

15.3. 共享变量声明

在共享变量共享之前,不需要区分其定义和声明。

  • 为了声明变量,写成如下形式,这样不仅声明了 i,也对 i 进行了定义,从而使编译器为 i 留出了空间
    int i; /* declares i and defines it as well */
    
  • 在其他文件中,想要共享这个变量的话,需要声明没有定义的变量 i,使用关键字 extern,extern 提示编译器变量 i 是在其他程序中定义的,因此不需要为 i 分配空间
    extern int i; /* declares i without defining it */
    

15.4. 保护头文件

为了防止头文件多次包含,将用 #ifndef 和 #endif 两个指令把文件的内容闭合起来。例如,如下的方式保护 boolean.h:

#ifndef BOOLEAN_H
#define BOOLEAN

#define TRUE 1
#define FALSE 0
typedef int Bool;

#endif

第16章 结构、联合和枚举

16.1. 结构使用 = 运算符复制

数组不能使用 = 运算符复制,结构可以使用 = 运算符复制:

struct { int a[10]; } a1, a2;
a1 = a2; /* legal, since a1 and a2 are structures */

16.2. 表示结构的类型

C 语言提供了两种命名结构的方法:

  • 声明 “结构标记”
    struct part {
      int number;
      char name[NAME_LEN+1];
      int on_hand;
    }; /* 结构标记的声明:右大括号的分号不能省略,表明声明的结束 */
    
    struct part part1, part2; /* 结构变量的声明:不能漏掉单词 struct 来缩写这个声明 */
    
    part part1, part2; /*** WRONG,part 不是类型名,没有 struct,part 没有任何意义 ***/
    
    struct part {
      int number;
      char name[NAME_LEN+1];
      int on_hand;
    } part1, part2; /* 结构标记的声明可以和结构变量的声明合在一起 */
    
  • 使用 typedef 来定义类型名
    typedef struct {
      int number;
      char name[NAME_LEN+1];
      int on_hand;
    } Part; /* 类型 Part 的名字必须出现在定义的末尾,而不是在 struct 后面 */
    
    Part part1, part2; /* 可以像内置类型一样使用 Part,由于 Part 是 typedef 定义的,所以不能写 Struct Part */
    

16.3. 联合的两个应用

  • 使用联合来节省空间
  • 使用联合来构造混乱的数据结构
    • 实现数组的元素类型是 int 和 float 的混合
      typedef union {
        int i;
        float f;
      } Number;
      
      Number number_array[1000]; 
      
      number_array[0].i = 5;
      number_array[1].f = 8.396;
      

16.4. 为联合添加 "标记字段"

为了判断联合中成员的类型,可以把联合嵌入一个结构中,且此结构还含有另一个成员 "标记字段",用来提示当前存储在联合中的内容的类型,如下所示:

#define INT_KIND 0
#define FLOAT_KIND 1

typedef struct {
  int kind; /* tag field */
  union {
    int i;
    float f;
  } u;
} Number;

void print_number (Number n){
  if (n.kind == INT_KIND){
    printf("%d", n.u.i);
  } else {
    printf("%g", n.u.f);
  }
}

16.5. 枚举

在一些程序中,我们可能需要变量只具有少量有意义的值,例如布尔类型应该只有两种可能,真值或假值。
C 语言为少量可能值的变量设计了枚举类型。

  • 定义枚举标记和枚举变量。类似 struct 的定义,可以通过 "枚举标记" 或者 typedef 两者方法定义枚举类型。
    enum bool { FALSE, TRUE }; /* 定义枚举标记 */
    enum bool b1, b2;
    
    typedef enum { FALSE, TRUE } Bool; /* 使用 typedef 进行定义类型 Bool */
    Bool b1, b2;
    
  • 枚举作为整数
    在系统内部,C 语言会把枚举变量和常量作为整数来处理。
    • 当没有为枚举常量指定值时,它的值是一个大于前一个常量的值(默认第一个枚举常量值为 0)
      enum colors { BLACK, WHITE, GRAY = 6, YELLOW, RED = 15 } ; /* BLACK = 0, WHITE = 1,YELLOW = 7 */
      
    • 枚举的值和整数混用,编译器会把 c 当作整型处理,而 BLACK, WHITE, GRAY, YELLOW, RED 只是数 0,1,2,3,4 的同义词。
      int i;
      enum { BLACK, WHITE, GRAY, YELLOW, RED } c;
      i = WHITE; /* i is now 1 */
      c = 0; /* c is now 0 (BLACK)*/
      c++; /* c is now 1 (WHITE)*/
      i = c + 2; /* i is now 3 */
      
  • 用枚举声明联合的 "标记字段",这样做的优势不仅远离了宏 INT_KIND 和 FLOAT_KIND (他们现在是枚举常量),而且阐明了 kind 的含义。
    typedef struct {
      enum { INT_KIND, FLOAT_KIND } kind;
      union {
        int i;
        float f;
      } u;
    } Number;
    

第17章 指针的高级应用

17.1. 内存分配函数

  • malloc:分配内存块,但是不对内存块进行初始化。
  • calloc:分配内存块,并且对内存块进行清除。
  • realloc:调整先前分配的内存块。

17.2. 空指针

当调用内存分配函数时,无法定位满足我们需要的足够大的内存块时,函数会返回空指针

  • 空指针是 "指向为空的指针",这是区别与所有有效指针的特殊值。
  • 程序员的责任是测试任意内存分配函数的返回值,并且在返回空指针时进行适当的操作。

对指针的处理惯用法如下:

p = malloc(1000);
if (p == NULL){
  /* allocation failed; take appropriate action  */
} /* 惯用法一 */

/***************************************************/

if ((p = malloc(1000)) == NULL){
  /* allocation failed; take appropriate action  */
} /* 惯用法二 */

/***************************************************/

p = malloc(1000);
if (!p){
  /* allocation failed; take appropriate action  */
} /* 惯用法三,C 语言中非空指针都为真,只有空指针为假 */

17.3. 使用 malloc 动态分配字符串

malloc 函数原型如下:

void *malloc ( size_t size );
  • malloc 分配 size 字节的内存块,并且返回指向此内存块的指针。
  • 通常情况下,可以把 void* 类型赋值给任何指针类型的变量。
  • 为 n 个字符串分配空间,可以写成 p = malloc(n + 1);

返回指向 "新" 字符串的指针的函数,没有改变原来的两个字符串:

char *concat( const char *s1, const char *s2 ){
  char *result;
  result = malloc(strlen(s1) + strlen(s2) + 1);
  if (result == NULL){
    printf("Error: malloc failed in concat\n");
    exit(EXIT_FAILURE);
  }
  strcpy(result, s1);
  strcat(result, s2);
  return result;
}

17.4. 使用 malloc 为数组分配内存空间

需要使用 sizeof 运算符来计算每个元素所需要的空间大小:

int *a;
a = malloc( n * sizeof(int) );

一旦 a 指向动态的内存块,就可以把 a 当作数组的名字。

17.5. 使用 calloc 为数组分配内存

calloc 函数原型如下:

void *calloc ( size_t nmemb, size_t size );
  • calloc 函数为 nmemb 个元素的数组分配内存空间,其中每个元素的长度都是 size 个字节。
  • 如果要求的空间无效,那么此函数返回空指针。
  • 在分配了内存以后,calloc 会通过对所有位设为 0 的方式进行初始化。

通过调用以 1 为第一个实际参数的 calloc 函数,可以为任何类型的数据项分配空间。

struct point {
  int x;
  int y;
} *p;
p = calloc(1, sizeof(struct point)); /* p 执行结构,且此结构的成员 x,y 都会被设为 0 */

17.6. 使用 realloc 函数调整先前分配的内存块

一旦为数组分配完内存,后面 realloc 函数可以调整数组的大小使它更适合需要。
realloc 的原型如下:

void *realloc (void *ptr, size_t size);
  • 当调用 realloc 时,ptr 必须指向内存块,且此内存块一定是先前通过 malloc 函数、calloc 函数或 realloc 函数的调用获得的。

  • size 表示内存块的新尺寸,可能会大于或小于原有尺寸。

  • C 标准中关于 realloc 函数的规则:

    • 当扩展内存块时,realloc 函数不会对添加进内存块的函数进行初始化。
    • 如果 realloc 函数不能按要求扩大内存块,那么它会返回空指针,并且原有内存块中的数据不会发生改变。
    • 如果 realloc 函数调用时以空指针作为第一个实际参数,那么它的行为就像 malloc 函数一样。
    • 如果 realloc 函数调用时以 0 作为第二个实际参数,那么它会释放掉内存块。
  • realloc 使用建议:

    • 当要求减少内存块大小时,realloc 函数应该在 "适当位置" 缩减内存块,而不需要移动存储在内存块中的数据。
    • 当要求扩大内存块大小时,realloc 函数应该始终试图扩大内存块而不需要对其进行移动。
    • 如果无法扩大内存块(因为内存块后面的字节已经用于其他目的),realloc 函数会在别处分配新的内存块,并把旧块中的内容复制到新块中。
    • 一旦 realloc 函数返回,一定要对指向内存块的所有指针更新,因为 realloc 函数可能移动了其他地方的内存块。

17.7. 释放内存 free 函数

free 函数原型如下:

void free(void* ptr);

使用 free 函数,只要把指向不再需要内存块的指针传递给 free 函数即可,如下所示:

p = malloc (...);
q = malloc (...);'
free (p);
p = q;
  • free 函数的实际参数必须是指针,而且此指针一定是先前被内存分配函数返回的。

悬空指针(dangling point)的问题:

  • 调用 free(p); 释放了 p 指向的内存块,但是 p 本身不会改变。如果忘记了 p 不再指向有效的内存块,可能会出现问题,如下所示:
    char *p = malloc(4);
    free(p);
    strcpy(p, "abc"); /*** WRONG。错误,修改 p 指向的内存是严重错误的,因为程序不再对此内存有控制权。***/
    
  • 悬空指针很难发现,因为几个指针可能指向相同的内存块。在释放内存块时,全部指针可能都留有悬空。

第18章 声明

18.1. 声明的语法

在大多数通过格式中,声明格式如下:

[ 声明的格式 ] 声明说明符 声明符;

声明说明符(declaration specifier)描述声明的数据项的性质。声明符(declarator)给出了数据项的名字,并可以提供关于数据项的额外信息。

  • 声明说明符分为以下三类:
    • 存储类型:存储类型一共四种,auto、static、extern 和 register。声明中最多出现一种存储类型,如果出现存储类型,则需要放在声明的首要位置。
    • 类型限定符:只有两种类型限定符,const 和 volatile。声明可以指定一个、两个限定符或者一个也没有。
    • 类型说明符:关键字 void、char、short、int、long、float、double、signed 和 unsigned 都是
      类型说明符。这些出现的顺序不是问题(int unsigned long 和 long unsigned int 完全一样)。类型说明符也包括结构、联合和枚举的说明。用 typedef 创建的类型名也是类型说明符。

18.2. 变量的存储类型

C 程序中每个变量都具有 3 个性质:

  • 存储期限
    • 变量的存储期限决定了为变量预留和释放内存的时间。
    • 具有自动存储期限的变量在所属块被执行时获得内存单元,并在块终止时释放内存单元。
    • 具有静态存储期限的变量在程序运行期间占有同样的存储单元,也就是可以允许变量无限期的保留它的值。
  • 作用域
    • 变量的作用域是指引用变量的那部分文本。
    • 变量可以有块作用域(变量从声明的地方一直到闭合块的末尾都是可见的)。
    • 变量可以有文件作用域(变量从声明的地方一直到闭合文件的末尾都是可见的)。
  • 链接
    • 变量的链接确定了程序的不同部分可以分享此程序的范围
    • 具有外部链接的变量可以被程序中几个(或许全部)文件共享。
    • 具有内部链接的变量只能属于单独一个文件,但是此文件中的函数可以共享这个变量。
    • 无链接的变量属于单独一个变量,而且根本不能被共享。

变量的默认存储期限、作用域和链接都依赖于变量声明的位置:

  • 块内部(包括函数体)声明的变量具有自动存储期限、块作用域,并且无链接
  • 在程序的最外层,任意块外部声明的变量具有静态存储类型、文件作用域和外部链接
  • 如下图所示:

对许多变量而言,默认的存储期限、作用域和链接是可以符合要求的。当这些性质无法满足要求时,可以通过指定明确的存储类型来改变变量的特性:auto、static、extern 和 register

  • auto 存储类型

    • auto 存储类型只对属于块的变量有效。
    • auto 类型是自动存储期限、块作用域,并且无链接。
    • 在块内部的变量,默认是 auto 类型,不需要明确指定。
  • static 存储类型

    • static 存储类型可以用于全部变量,不需要考虑变量声明的位置。
    • 块外部声明变量和块内部声明变量的效果不同。
      • 在块外部时,static 说明变量具有内部链接。
      • 在块内部时,static 把变量的存储期限从自动变成了静态。
      • 如下所示:
  • extern 存储类型

    • extern 存储类型可以使几个源文件共享同一个变量。
    • extern 声明中的变量始终具有静态存储期限。
    • 变量的作用域依赖于变量的位置。
      • 变量在块内部,具有块作用域。
      • 变量在块外部,具有文件作用域。
    • extern 变量的链接不是确定的。
      • 如果变量在文件中较早的位置(任何函数定义的外部)声明为 static,那么它具有内部链接。
      • 否则(通常情况),变量具有外部链接。
    • 如下所示:
  • register 存储类型

    • register 存储类型要求编译器把变量存储在寄存器中,而不是内存中。
    • 指明 register 类型是一种要求,而不是命令。
    • register 只对声明在块内的变量有效,和 auto 类型一样是自动存储期限、块作用域,并且无链接。
    • 跟 auto 相比,由于寄存器没有地址,所以 register 存储类型使用取地址符 & 是非法的,

18.3. 函数的存储类型

函数的声明(和定义)可以包含存储类型,但是选项只有 extern 和 static。

  • 函数在默认情况(不指明存储类型)下,具有外部链接,允许其他文件调用此函数。
  • extern 说明函数具有外部链接,函数默认是 extern 类型,不需要明确使用 extern。
  • static 说明函数具有内部链接,只能在定义函数的文件内调用此函数。
  • 函数的形式参数具有和 auto 变量相同的性质:自动存储期限、块作用域,和无链接。
    • 唯一能用于说明形式参数存储类型的就是 register。

四种类型中最重要的就是 extern 和 static 了,auto 没有任何效果,而现代编译器已经使得 register 变得废弃无用了。

18.4. 类型限定符 const

  • const 用来声明一些类似于变量的对象,但这些变量是 “只读” 的。程序可以访问 const 型对象的值,但无法改变它的值。
  • 不同于宏,不可以把 const 型对象用于常量表达式。
    const int n = 10;
    int a[n];  /*** WRONG ***/
    
  • 我们使用 const 主要是为了保护存储在数组中的常量数据。

18.5. 解释复杂声明

两条简单的规则可以用来理解任何的声明:

  • 始终从内往外读声明符。换句话说,定位用来声明的标识符,并且从此处的声明开始解释。
  • 在作选择时,始终先是 []() 后是 *
    • 如果 * 在标识符前面,而在标识符后面有 [],那么标识符表示数组而不是指针。
    • 如果 * 在标识符前面,而在标识符后面有 (),那么标识符表示函数而不是指针。

18.6. 初始化式

  • 为了方便,C 语言允许在声明变量时为他们指定初始值。
  • 为了初始化变量,可以在声明符后面写 = ,然后在其后再跟上初始化式(不要把声明中的符号 = 和赋值运算符混淆,初始化和赋值不一样)。
  • 控制初始化式的额外规则:
    • 具有静态存储期限的变量的初始化式必须是常量。
    • 如果变量具有自动存储期限,那它的初始化式不需要常量。
    • 用大括号封闭的数组、结构或联合的初始化式只能包含常量表达式,不能有变量或函数调用。

第20章 低级程序设计

20.1. 结构中的位域

C 语言可以声明结构中其成员表示位域的结构。

  • 例如,使用 16 位来存储日期,其中 5 位用于日,4 位用于月,7 位用于年。
  • 利用位域,可以如下定义:
    struct file_data {
      unsigned int day : 5;
      unsigned int mouth : 4;
      unsigned int year : 7;
    }  
    

控制位域存储的技巧:

  • 忽略位域的名字,未命名的位域通常用来作为字段间的 "填充",以保证其他位域存储在适当的位置。
    struct file_data {
      unsigned int  : 5;  /* not used */
      unsigned int mouth : 4;
      unsigned int year : 7;
    }  
    
  • 指定未命名的字段长度为 0。长度为 0 的位域是给编译器一个信号,告诉编译器将下一个位域放在一个存储单元的起始位置。
    struct file_data {
      unsigned int a : 4;
      unsigned int : 0;  /* 如果存储单元是 8 位,编译器会给 a 分配 4 位,跳过余下的 4 位到下一个存储单元,给 b 分配 8 位。 */
      unsigned int b : 8;
    }  
    

20.2. volatile 类型限定符

使用 volatile 类型限定符,我们可以通知编译器程序使用了内存空间 "易变" 的数据(例如从键盘缓冲区读取的数据)。


第21章 标准库

21.1 标准库概述

以下是标准库中的 15 个头。

  • <assert.h> 诊断
    • 仅包含 assert 宏,可以在程序中插入该宏,从而检测程序状态。一旦任何检查失败,程序终止。
  • <ctype.h> 字符处理
    • 包括用于字符分类及大小写转换的函数。
  • <errno.h> 错误
    • 提供了 errno(error number)。errno 是一个左值,可以在调用特定函数后进行检测,来判断调用过程中是否有错误发生。
  • <float.h> 浮点型的特性
    • 提供了用于描述浮点类型特性的宏,包括值的范围及精度。
  • <limits.h> 整形的大小
    • 提供了用于描述整数类型和字符类型的宏,包括它们的最大值和最小值。
  • <locale.h> 本地化
    • 提供一些函数来帮助程序适应针对一个国家或地区的特定行为方式。
  • <math.h> 数学计算
    • 提供了大量用于数学计算的函数。
  • <setjmp.h> 非本地跳转
    • 提供了 setjmp 函数和 longjmp 函数。
  • <signal.h> 信号处理
    • 提供了用于异常情况(信号)处理的函数,包括中断和运行时错误。
  • <stdarg.h> 可变实际参数
    • 提供函数可以处理不定个数个参数的工具。
  • <stddef.h> 常用定义
    • 提供了经常使用的类型和宏。
  • <stdio.h> 输入/输出
    • 提供大量用于输入输出的函数。
  • <stdlib.h> 常用使用程序
    • 包含大量无法归类于其他头的函数。
  • <string.h> 字符串处理
    • 提供用于字符串操作的函数。
  • <time.h> 日期和时间
    • 提供相应的函数来获取日期和时间、操纵时间和以多种方式显示时间。

第22章 输入 / 输出

22.1. 标准流

22.2. fopen 函数打开文件的模式

  • 打开文本文件的模式
  • 打开二进制文件的模式

22.3. 从命令行打开文件

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE *fp;
    if (argc != 2)
    {
        printf("usage: canopen filename\n");
        return 2;
    }

    if ((fp = fopen(argv[1], "r")) == NULL)
    {
        printf("%s can't be opened\n", argv[1]);
        return 1;
    }

    printf("%s can be opened\n", argv[1]);
    fclose(fp);
    return 0;
}

22.4. ...printf 类函数

  • 使用符号 * 填充格式串中的常量

22.5. ...scanf 类函数

22.6. 复制文件

/* Copies a file */

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

int main(int argc, char *argv[])
{
    FILE *source_fp, *dest_fp;
    int ch;

    if (argc != 3)
    {
        fprintf(stderr, "usage: fcopy source dest\n");
        exit(EXIT_FAILURE);
    }

    if ((source_fp = fopen(argv[1], "rb")) == NULL)
    {
        fprintf(stderr, "Can't open %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    if ((dest_fp = fopen(argv[2], "wb")) == NULL)
    {
        fprintf(stderr, "Can't open %s\n", argv[2]);
        fclose(source_fp);
        exit(EXIT_FAILURE);
    }

    while ((ch = getc(source_fp)) != EOF)
    {
        putc(ch, dest_fp);
    }

    fclose(source_fp);
    fclose(dest_fp);

    return 0;
}

参考

推荐阅读更多精彩内容

  • 指针是C语言中广泛使用的一种数据类型。 运用指针编程是C语言最主要的风格之一。利用指针变量可以表示各种数据结构; ...
    朱森阅读 2,676评论 3 44
  • 酒是一种营养价值很高的饮料。适量饮用可增加高密度脂蛋白含量,减少动脉内胆固醇,从而防止心脏病;并有促进食欲,...
    若爱养生阅读 2,366评论 0 1
  • UI 稿 功能描述 默认选中全部,即获取全部列表数据。当点击“通识类”或“实训类”按钮时,切换选中选项,改变路由,...
    baby熊_熊姐阅读 639评论 2 0
  • 工作之余,习字两篇,《边城》看了一半,简书溜达半天,与两个笔友相谈甚欢。
    无为而字阅读 81评论 2 5
  • 百日阅读第 19天 分享人:雪舞 学号:55 书名:《当仓央嘉措遇上纳兰容若》 纳兰容若篇 第二章人生初见恨秋凉 ...
    雪山飞狐儿阅读 48评论 0 0