• 这本书内容看懂不难,但想要在用的时候避免出现其中问题可能还和实践与记忆力相关,所以推荐边写边查。

下面的是我感觉自己没遇见过、有趣或重要的点

词法陷阱

  1. 单双引号的意义

单引号括起字符:整数
双引号括起字符:指针

  1. 判断是否允许嵌套注释的例子

    1. int nest = /*/*/ 0 */**/ 1;
    2. // 如果允许,nest = 1
    3. // 如果不允许,nest = 0 * 1;

    语法陷阱

  2. 可以将常量强制转化为各种类型的指针

    1. int *x;
    2. char *c = "kkkkkk";
    3. x = (int*)1;
    4. x = (int*)c;
    5. printf("%p\n", x);
  3. 优先级记忆

  • 最高:() [] -> .
  • 单目>双目>三目>逗号
    • 双目:算术>移位>关系>逻辑(单>双,&>&&)> 赋值 > 条件(即三目)
  1. else悬挂 ```c if(x == 0) if(y == 0)
    1. error();
    else { z = x + y; f(&z); } /*
  • key:第一个if处未加花括号,
  • 解释:c语言中else特性为始终与同一对括号内最近的未匹配if结合
  • / ```

    语义陷阱

  1. c语言中只有四个运算符(&&,||,?:,逗号)存在求值顺序
  • &&和||,先运算左侧再右侧
  • a?b:c,先算a,根据a的值计算b、c
  • 逗号运算符,先对左侧操作数求值,然后丢弃该值,再对右侧操作数求值。例:x =(a,b)先对a进行运算,然后丢弃a,最后求b,将b赋值给x
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. void mcpy(char *&dst, char *src, int n)
  4. {
  5. while(n--)
  6. {
  7. *dst++ = *src++;
  8. printf("%p\n", dst);
  9. }
  10. }
  11. void buffflush(char *test, char *idx)
  12. {
  13. printf("flush : %s\n", test);
  14. idx = test;
  15. }
  16. void bufwrite(char *p, int n, char *bufptr, char *buffer)
  17. {
  18. while(n > 0)
  19. {
  20. int k, rem;
  21. if(bufptr == &buffer[9])
  22. buffflush(buffer, bufptr);
  23. rem = 10 - (bufptr - buffer);
  24. k = rem > n ? n : rem;
  25. mcpy(bufptr, p, k);
  26. p += k;
  27. n -= k;
  28. }
  29. }
  30. int main(int argc, char **argv)
  31. {
  32. char str[200] = "1234567890abcdefghijklmnopqrstuvwxyz";
  33. char *s = (char *)malloc(sizeof(char)*200);
  34. char *buff, *bufidx;
  35. buff = (char*)malloc(sizeof(char)*10);
  36. bufidx = buff;
  37. printf("%p\n", buff);
  38. mcpy(bufidx, (char*)str, 30);
  39. printf("%p\n", bufidx);
  40. printf("%s\n%s\n", bufidx, buff );
  41. // bufidx和buff位置依然相同,你能找出错在哪吗?
  42. // bufwrite(s, 22, bufidx, buff);
  43. return 0;
  44. }
  45. /**
  46. * 原因:上面传递的是指针,虽然指针可以修改指向的变量,但是其自身依旧是临时变量
  47. * 解决:要真正修改其自身地址,c可以使用二维指针,c++可以使用指针引用,所以上面的写法是错误的
  48. */
  • 解决方法,修改后代码 ```c

    include

    include

    include

void mcpy(char *dst, char src, int n) { while(n—) { (dst) = src++; (dst)++; printf(“%p\n”, *dst); } }

void buffflush(char test, char **idx) { printf(“flush : %s\n”, test); memcpy(test, “”, 10); idx = test; }

void bufwrite(char p, int n, char bufptr, char *buffer) { while(n > 0) { int k, rem; if(bufptr == &buffer[10]) buffflush(buffer, &bufptr); rem = 10 - (bufptr - buffer); k = rem > n ? n : rem; mcpy(&bufptr, p, k); p += k; n -= k; } }

int main(int argc, char **argv) { char str[200] = “1234567890abcdefghijklmnopqrstuvwxyz”;

  1. char *s = (char *)malloc(sizeof(char)*200);
  2. char *buff, *bufidx;
  3. buff = (char*)malloc(sizeof(char)*10);
  4. bufidx = buff;
  5. bufwrite(str, 22, bufidx, buff);
  6. return 0;

}

  1. 2. 如何在a文件之内调用b文件中的变量和函数
  2. ```c
  3. // a.h
  4. #ifndf _A_H_
  5. #define _A_H_
  6. #include <stdio.h>
  7. int arg = 0x10000000;
  8. double square(double x);
  9. #endif
  10. // a.c
  11. #include "a.h"
  12. double square(double x)
  13. {
  14. return x * x;
  15. }
  16. // b.c
  17. #include "a.h"
  18. // 调用别的文件中变量,需要加extern,说明别的文件中已经声明且赋值过
  19. extern int arg;
  20. // 调用其他文件中函数,仅需要将所需函数头文件导入,同时再次声明即可
  21. double square(double x);
  22. int main(int argc, char **argv)
  23. {
  24. printf("%lf\n", square(1.22));
  25. return 0;
  26. }
  1. extern、static
  2. 形参、实参、返回值
  • 如果一个函数在被定义之前被调用,那么它的返回类型就被默认为整型
  • 如果main函数和square函数在不同的文件中
    • 必须在调用它的文件中声明square函数 ```c double square(double);

main() { printf(“%g\n”, square(0.3)); }

  1. - 如果一个函数没有floatshortchar类型的参数,在函数声明中完全可以省略参数类型的说明,但是要求自己注意函数调用时实参的输入,示例如下
  2. ```c
  3. #include <stdio.h>
  4. int func();
  5. int main(int argc, char **argv)
  6. {
  7. func(1, 2, 3);
  8. return 0;
  9. }
  10. int func(int x, int y, int z)
  11. {
  12. printf("%d\n", x + y + z);
  13. return x + z + y;
  14. }
  1. 检查外部类型
  • 假设a.c中声明int a,而b.c中声明extern long a,这里可能会引起几种情况
    • 编译器报错
    • 恰好能工作,比如32位机器的long和int相同
    • 两个实例虽然要求存储空间大小不同,但它们共享存储空间的方式恰好满足:赋给其中一个值,对另一个也有效,比如,long的低位和int共享存储空间,当给long类型赋值时,恰好把低位赋给int,本来错误的程序看起来似乎能正常工作
    • 共享存储空间,使得对其中一个赋值,其效果相当于同时给另一个服了完全不同的值,程序不能正常工作

库函数

  1. 返回整型的getchar ```c // getchar函数原型为 int getchar(void);

// 它返回一个整型数据,但是如果按照下面写法则有可能导致错误

include

int main() { char x; while((x = getchar()) != EOF) putchar(); }

// int会被截断,转化为char,我们知道int的其他部分会被舍弃只留下低八位 /*

  • 以下几种可能:
    1. 被截断之后,刚好是EOF,被迫跳出循环
    1. 被截断之后,永远得不到EOF,死循环
    1. 似乎能正常工作,是因为编译器实现不正确,依然将getchar返回值与EOF比较,
  • 而非用c与EOF比较 */ ```
  1. 更新顺序文件

想要对文件进行自由交错的读写操作,单纯通过fopen(filename, “r+”)是不行的。为了保持与过去不能同时进行读写操作的程序的向下兼容性,一个输入操作不能随后直接紧跟一个输出操作,反之亦然。
何为向下兼容性(Downward Compatibility)呢?

  • 在计算机中指在一个程序或者类库更新到较新的版本后,用旧的版本程序创建的文档或系统仍能被正常操作或使用,或在旧版本的类库的基础上开发的程序仍能正常编译运行的情况。 ```c // 上面所说的不行,如下 FILE *fp; fp = fopen(filename, “r+”); struct record rec;

while(fread((char )&rec, sizeof(rec), 1, fp) == 1) // rec需要被转为char { // do some operations to rec if(/ rec must be rewritten in file /) { fseek(fp, -(long)sizeof(rec), 1); fwrite((char )&rec, sizeof(rec), 1, fp); // rec需要被转为char } }

// 看起来已经很小心地转化了类型,同时也在fread和fwrite之间填入fseek,解决了历史遗留问题 // 但依然不对能看出来吗?

// 问题出在read、seek、write、seek、read… // 上面只有read、seek、write、read、seek,没有真正的全部插入seek

  1. 3. 缓冲输出与内存分配
  2. ```c
  3. setbuf(stdout, buf)
  4. fflush(FILE *fp) 清空缓存
  1. 使用errno检测错误
  • 直接使用if(errno)检测,不能保证之前某些函数修改过errno
  • 在if(errno)前添加errno=0,但是不能保证某些成功调用的函数依然有可能修改errno的情况。
    • 比如:使用开启新文件的函数,其中需要先检查有没有同名文件,如果没有则修改errno,再新开一个文件
  • 比较好的方式是:先检测函数返回值,确定函数出错后再进行errno的判断
    1. /* 调用库函数 */
    2. if(判断函数返回值)
    3. {
    4. 检查errno
    5. }
  1. 库函数signal
    1. #include <signal.h>
    2. signal(signal type, handler function);
  • 信号在C程序任何时候有可能发生,甚至是在某些复杂库函数(如malloc)的执行过程中,就安全性来讲,信号处理函数不应该调用上述函数。可能出现一下不好情况:
    • malloc在执行过程中被信号中断,malloc函数用来跟踪内存的数据结构可能只有部分被更新。如果signal处理函数再调用malloc函数结果可能是malloc函数用到的数据结构完全崩溃
    • 在signal处理函数中使用longjmp退出,也不安全:信号可能发生在某些库函数未完全更新数据结构过程中
  • 结论:信号处理异常棘手,我们要采取守势,让signal处理函数尽可能简单

    预处理器

  1. 宏定义中的空格 ```c

    define f (x) ((x) - 1)

上面f后多一个空格,代表(x) ((x) - 1)

上述情况出现在宏定义,宏调用则是f(x)和f (x) 表示相同含义

  1. 2. 宏不是类型定义,在声明多个变量的情况下最好还是用typedef,考虑可移植性的情况也推荐使用typedef
  2. <a name="QS3lH"></a>
  3. ## 可移植性缺陷
  4. 1. 字符是有符号整数还是无符号整数
  5. charint,无符号整数只需要在多余位上添加0,有符号整数应该同时复制符号位
  6. ```c
  7. #include <stdio.h>
  8. int main(){
  9. printf("%d\n", sizeof(int)); // at first, show size of integer on your pc
  10. unsigned char x = 0xff;
  11. printf("%u\n", x);
  12. printf("%d\n", x);
  13. char y = 0xff;
  14. printf("%u\n", y);
  15. printf("%d\n", y);
  16. return 0;
  17. }
  18. 输出结果是:
  19. 4
  20. 255
  21. 255
  22. 4294967295
  23. -1

这里面反映出一些有趣的点:

  • 如果直接声明为unsigned char,那么无论是什么编译器将该字符转换为整数都是需要填充0
  • 如果是一个char,将其强制转换为unsigned也不是直接得到等价无符号整数(填充0类型),其中是因为转为无符号整数时,编译器是将其先转化为int
  1. 除数运算时发生的截断 ```c q = a / b; r = a % b;

假定b大于0

// 我们希望a、b、q、r保持如下关系

  1. q * b + r == a
  2. 如果改变a的正负号,q会被改变符号,但不会改变绝对值
  3. 当b > 0时,我们希望保证r >= 0且r < b

/**

############################简要测试程序如下

*/

include

include

int main(){ int a, b, q, r; while(~scanf(“%d %d”, &a, &b)) { q = a / b; r = a % b; int newQ = (-a) / b;

  1. printf("q : %d\nr : %d\n", q, r);
  2. // rule 1 test
  3. if(q * b + r == a)
  4. printf("Obey the first rule.\n");
  5. else
  6. printf("Violate the first rule.\n");
  7. // rule 2 test
  8. if(newQ == (-q) && abs(q) == abs(newQ))
  9. printf("Obey the second rule.\n");
  10. else
  11. printf("Violate the second rule.\n");
  12. // rule 3 test
  13. if(b > 0 && r >= 0 && r < b)
  14. printf("Obey the third rule.\n");
  15. else if(b < 0)
  16. printf("Do not reach the third rule.\n");
  17. else
  18. printf("Violate the third rule.\n");
  19. }
  20. return 0;

}

// 对于取模运算,c语言做到将结果的正负号和被取模数相同,无论模数是什么符号 /**

############################简要测试程序如下

*/

include

include

int main() { int x; while(~scanf(“%d”, &x)) { printf(“%d %% 3 = %d\n”, x ,x % 3); printf(“%d / 3 = %d\n”, x, x / 3); printf(“%d %% -3 = %d\n”, x, x % -3); printf(“%d / -3 = %d\n”, x, x / -3); }

  1. return 0;

}

```