为了账号安全,请及时绑定邮箱和手机立即绑定

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

标签:
C

第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),指定了要显示的最小字符数量。

      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);
    • 如果要打印的数值比 m 个字符少,那么值在字段内是右对齐的(换句话说,在数值前面放置额外的空格)。

    • 如果要打印的数值比 m 个字符多,那么字段宽度会自动扩展为需要的尺寸,而不会丢失数字。

    • 在 m 前放上一个负号,会发生左对齐。

    • 举例如下:

webp

  • p 表示的是精度(precision),p 的含义依赖于转换说明符(conversion specifier) X 的值,X 表明需要对数值进行哪种转换。常见的转换说明符有:

      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);
    • 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 的转换将不显示尾随零。

    • 举例如下:

webp

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

webp

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

webp

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)
    • abc 小于 bcdabc 小于 abdabc 小于 abcd

    • 比较两个字符串时,strcmp 会查看表示字符的数字码。以 ASCII 字符集为例:

    • 所有大写字母(65 ~ 90)都小于小写字母(97 ~ 122)。

    • 数字(48 ~ 57)小于字母。

    • 空格符(32)小于所有打印字符。

    • 比较 s1 和 s2,根据 s1 是否小于、等于、大于 s2,会返回小于、等于、大于 0 的值。

    • strcmp 利用字典顺序进行字符串比较,比较规则如下:

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

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

webp

  • 字符串指针数组

char *planets[] = {"Mercury","Venus","Earth","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto"};

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

webp

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



作者:gfson
链接:https://www.jianshu.com/p/3e6ced8bad88


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
全栈工程师
手记
粉丝
228
获赞与收藏
996

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消