C陷阱与缺陷 个人阅读笔记

C陷阱与缺陷 个人阅读笔记

  • 一、词法
  • 二、语法
  • 三、语义
    • 3.1 指针与数组
    • 3.2 非数组的指针
    • 3.3 作为参数的数组声明
    • 3.4 避免“举隅法”:复制指针不等同于复制数据!!!
    • 3.5 空指针并非空字符串
    • 3.6 边界计算与不对称边界
    • 3.7 求值顺序
    • 3.8 &&、 ||、 !与&、|、~
    • 3.9 整数溢出
    • 3.10 为main函数提供返回值
  • 四、连接
    • 4.1 连接器
    • 4.2 声明与定义
    • 4.3 命名冲突与static
    • 4.4 形参、实参、返回值
    • 4.5 检查外部类型
    • 4.6 头文件
  • 五、库函数
    • 5.1 getchar()
    • 5.2 文件读写的兼容性问题
    • 5.3 缓冲输出与内存分配
    • 5.4 使用errno检测错误
    • 5.5 库函数signal
  • 六、预处理器
  • 七、可移植性缺陷
  • 八、建议

一、词法

  • 1.1 =和==

  • 1.2 位操作符&、| 和逻辑操作符&&、||

  • 1.3 编译器优先匹配多个字符的操作符
    例:a—b; 相当于(a–) -b; 再者y=x/p; 会直接匹配注释符‘ /
    另外,a++的结果不能作为左值,a+++++b编译器解释为((a++)++)+b但是会出现语法错误

  • 1.4 一个整形常量以0开头,该常量会被视为8进制数

  • 1.5 '‘和"",’'括一个字符表示一个整数,""括一个字符代表一个指针
    例:char *str = ‘/’; //会报错
    'yes’合法但不同编译器会解释成不同的意思。

  • 练习1-1 允许嵌套注释,想注释大段代码段可以使用宏
    例:
    #if 0

    #endif

二、语法

  • 2.1 理解声明,函数指针、指针函数等;(*(void (*)())0)();
  • 2.2 运算符优先级,不清楚就加括号
() [] {} -> .
! ~ ++ -- - (type) * &(取址) sizeof   //自右至左
* / %
+ -
<<  >>
< <= > >=
== !=
&(按位与)
^
|
&&
||
?:              //自右至左
赋值运算符       //自右至左
,
  • 2.3 注意分号空语句
  • 2.4 switch语句的break
  • 2.5 函数调用即使没有参数也要包含参数列表
  • 2.6 if else对齐缩进,加括号

三、语义

3.1 指针与数组

int a[5][5];
int (*p)[5]; //*p表示一个int[5]型的数组,则p是指向这样一个数组的指针。
int *q; //q是指向一个整型变量的指针。
for(p = a; p < &a[5]; p++) //a[5]是一个int[5]型的数组
	for(q = *p; q < &(*p)[5]; q++) //(*p)[5]表示数组最后一个整型变量,将其地址与q比较
		*q=0; //将a中的元素清0

3.2 非数组的指针

  1. 字符串 char *r; 一个字符串的实际长度应该是strlen( r ) + 1(+1是结束标志符)
  2. malloc函数的使用:
    malloc声明的内存必须在不使用时及时释放
    malloc有获取失败的可能,此时会返回一个空指针,要在使用前进行判空操作。
    例:
char *r, *malloc();
	r = malloc(strlen(s) + strlen(t) + 1);
	if(!r) {
		complain();
		exit(1);
	}
	/*...*/
	free(r);

3.3 作为参数的数组声明

  • c语言中数组作为参数实际上是传入一个指向数组第一个元素的指针。

3.4 避免“举隅法”:复制指针不等同于复制数据!!!

  • 如果多个指针指向同一份数据,那通过一个指针对该数据进行修改,其他指针指向的数据(其实就是同一份)当然也就发生了更改。

3.5 空指针并非空字符串

  • NULL在c语言中define为(void*)0

3.6 边界计算与不对称边界

int a[5];
int* p;
/*虽然直接引用a[5]是不合法的,但是c语言允许用&获取实际上数组出界的地址甚至改变其的值*/
for (p = a; p != &a[5]; p++)
{
	*p = 0;
}

3.7 求值顺序

  • && 和 || 先对左侧求值,需要时才对右侧求值。
    例:if(count != 0 && sum / count < smallaverage)
    即使count为0,由于左侧为假,则右侧不被计算,因此也不会报被除数为0的错误。
  • a ? b : c 先对 a 求值再根据 a 的值求 b 或 c 的值。
  • 逗号运算符:(分隔函数参数的逗号不是运算符)
    先对左侧求值,然后将该值丢弃,再对右侧求值。

3.8 &&、 ||、 !与&、|、~

  • 逻辑运算与位运算的区别

3.9 整数溢出

  • 无符号整数运算结果是对2的n次方取模,因此不存在溢出,一个有符号整数在与无符号整数运算时会转换成无符号整数。

    当两个有符号整数运算时,就有可能溢出。
    判断溢出:
    1. if ((unsigned)a + (unsigned)b > INT_MAX)
    2. if (a > INT_MAX - b)

3.10 为main函数提供返回值

  • 成功return 0; 失败return 一个非零数。

四、连接

4.1 连接器

  • 连接器将若干c源程序合并成一个整体,将编译器生成的若干目标模块整合成一个载入模块,需要解决重名冲突,解析各模块对其他模块/库的引用。

4.2 声明与定义

  • 在 .h中声明, .c中定义

4.3 命名冲突与static

  • static可以修饰变量和函数,可以对外部源文件隐藏。

4.4 形参、实参、返回值

  • 如果函数参数没有float、char、short类型,则在函数声明中可以省略参数类型的说明。
    这些类型若不声明,传入参数将自动转为int类型。

4.5 检查外部类型

  • extern定义的变量类型不同导致连接时的错误,特别是数组和指针是不一样的类型。
  • 未声明的标识符后跟一个开括号,默认其返回类型为整型。(解释为extern int xxx();

4.6 头文件

  • 外部类型要在头文件声明。

五、库函数

5.1 getchar()

  • 返回整数,若用char类型接收可能无法容下所有可能的字符,比如EOF

5.2 文件读写的兼容性问题

  • 为了兼容过去不能同时对一个文件读写,一个输入操作不能立刻接一个输出操作。
    可以使用fseek()函数穿插:
int fseek(FILE *stream, long int offset, int whence)
stream 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
offset 这是相对 whence 的偏移量,以字节为单位。注意sizeof的结果是unsigned int。
whence 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:
  • whence的取值范围:
常量 描述
SEEK_SET 文件的开头
SEEK_CUR 文件指针的当前位置
SEEK_END 文件的末尾

5.3 缓冲输出与内存分配

  • 缓冲区一般定义在全局,或设置为static
  • setbuf(stdout, (char*)0); //强制不允许对输出进行缓冲

5.4 使用errno检测错误

  • extern int errno

5.5 库函数signal


  • signal(signal_type, handler_function);
  • 信号是真正意义上的“异步”,由于信号出现在很多复杂库函数中,因此信号的处理函数不能调用这些库函数。类似地,在信号函数中使用longjmp退出也是不安全的。

六、预处理器

  • 宏只是文本替换,没有函数调用的开销。getchar和putchar经常被实现为宏。
  • 6.1 注意宏中的空格
  • 6.2 注意参数的副作用 括号、自增自减的副作用等
  • 6.3 宏不是语句
    在宏中使用if要注意它与上下文衔接的缩进。
    PS:若实在想使用条件判断,可以考虑直接使用 || 表达式的方式,例如
    ((void) ((e) || f()))
    当e为真,后半不会计算,f()将不会运行;e为假,后半才会计算,f()才会运行。
  • 6.4 宏不是类型定义
    特别是需要给指针类型取别名时,如果使用宏定义的展开定义多个新变量,它将达不到想要的结果。例如:
#define T1 struct foo * 		
typedef struct foo *T2; 		
T1 a, b;   //相当于struct foo *a, b; 这导致a是指针类型,b却是foo类型 		
T2 a, b;   //a,b都是指针类型

七、可移植性缺陷

  • 7.1 c语言标准的变更
    比如函数声明时的参数类型能否声明。

  • 7.2 标识符名称的限制
    ANSI C标准保证的是前6个字符的区分,且不区分大小写。
    当移植的编译器真的仅仅按这个标准,那么标识符的命名就必须考虑恰当。

    目前的标准,外部标识符是6个字符,内部是32个字符。

C陷阱与缺陷 个人阅读笔记_第1张图片

  • 7.3 整数的大小
    short、int、long,int足以容纳任何数组的下标
    short和int至少16位,long至少32位
    为了可移植性,可以对这几种类型用typedef取别名来使用

  • 7.4 字符是有符号整数还是无符号整数

  • 7.5 移位运算符
    无符号数移位是补零;有符号数移位可以填0,也可以补符号位。

    移位的取值范围是0到位数n-1,即一次操作不允许将某个数所有位都移出

    无符号数做除2的操作,使用右移一位比用/2的效率高。

  • 7.6 内存位置0
    NULL指针define为了(void*)0,这也意味着它指向内存地址0,一般情况下,它只允许在赋值和比较运算时使用,其他情况是非法的。
    在不同的c语言编译器下,对于内存地址0有不同的限制,硬件级读保护/只读/可写可读,第三种情况当对内存地址0非法写,可能会修改操作系统的内容。
    因此为了良好的可移植性和程序的健壮性,严格禁止对NULL指针的其他操作。

如图是在vcruntime.h找到的NULL的定义:
C陷阱与缺陷 个人阅读笔记_第2张图片
keil5环境stdio.h中的定义是直接定义为了0:
keil NULL

  • 7.7 除法运算时发生的截断
    对于c语言的除法和取余,即a=q*b+r,只满足以下的性质:
    当a>=0,b>0时,有|r|<|b|,且r>=0。
    这意味着a,b出现负数时并不能保证余数和商的符号,不同编译器对此也有不同的安排,因此建议为了可移植性,取余和求商的操作在无符号数上进行,需要符号时再自己转换。
  • 7.8 随机数的大小
    rand函数在不同c实现输出的随机数的最大值可能存在差异,ANSI C标准中定义了RAND_MAX但在较早的c实现是没有的。
  • 7.9 大小写转换
    即库函数toupper( c )和tolower( c ),最初是用宏实现,且不判断c的合法性。
    目前ctype.h中定义的 toupper 和 tolower 都是函数。
  • 7.10 首先释放,然后重新分配
    UNIX的realloc函数可以保证min(oldsize, newsize)区域的内存块的数据保持不变,这意味着
    free(p); p = realloc(p, newsize);
    也是合法的(只要free和realloc之间衔接的够快,就没有问题)

八、建议

  • 尽量用括号表明意图
  • 判断等于的常量可以放在左边,这样即便不小心用了赋值运算符,编译器也会报错
  • 考查特例
  • 使用不对称边界
  • 潜伏的bug:
    特别是不同c实现下某些函数的参数限制,移植性情况等,这通常在程序逻辑上是发现不了的
  • 防御性编程:
    比如对已知非法情况的操作;
    再比如即使自己认为所有非法条件下的操作都已经写了具体的操作,但还是需要对其他情况给出一个适当的处理,以防止没有考虑到的非法情况造成系统的崩溃。(具体来说,比如所有 if 都有 else)

你可能感兴趣的:(书籍学习笔记,c语言)