关于c:程序人生-我的C陷阱与缺陷读书笔记

2次阅读

共计 4007 个字符,预计需要花费 11 分钟才能阅读完成。

本文首发于 2014-08-04 17:56:55

第一章 词法“陷阱”

1. = 不同于 ==

if(x = y)
         break;

实际上是将 y 赋给 x,再查看 x 是否为 0。

如果真的是这样预期,那么应该改为:

if((x = y) != 0)
         break;

2. & 和 | 不同于 && 和 ||

3. 词法剖析中的“贪婪法”

编译器将程序分解成符号的办法是:从左到有一个一个字符的读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断曾经读入的两个字符组成的字符床是否可能是一个符号的组成部分;如果可能,持续读入下一个字符,反复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。例如:

y = x/*p; 会被解析为:/* 正文符号

4. 整型常量

010(八进制数) 不同于 10(十进制)。

5. 字符与字符串

首先是单引号与双引号的区别:

  • 用单引号括起来的一个字符示意一个整数(ASCII 码),而双引号括起来示意一个指针。

第二章 语法“陷阱”

1. 了解函数申明

弄懂(*(void(*)())0)(); // 首地址为 0 的函数。

float (*h)(): h 是一个指向返回值为浮点型的函数的指针

所以,(float (*)()) 示意一个“指向返回值为浮点型的函数的指针”的类型转换符。

fp(): 是 (*fp)() 的简写。

*fp(): 是 *((*fp) ())的简写。

(*0)();

尽管上式编译器不认,但能够把 0 转换为指向“返回值为 void 的”函数的指针,所以 0 可变为:(void(*) ()) 0,代入(*0)(),失去:

(*( void(*) ()) 0) ()

该式子用等价于:

typedef void  (*func) ( );
(*( func) 0 ) ();

相似的,signal.h 中对 signal 函数的申明:

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

2. 运算符优先级的问题

优先级 运算符 名称或含意 应用模式 联合方向 阐明
1 [] 数组下标 数组名[常量表达式] 左到右
1 () 圆括号 (表达式) 函数名(形参表) 左到右
1 . 成员抉择(对象) 对象. 成员名 左到右
1 -> 成员抉择(指针) 对象指针 -> 成员名 左到右
2 负号运算符 - 表达式 右到左 单目运算符
2 (类型) 强制类型转换 (数据类型)表达式 右到左
2 ++ 自增运算符 ++ 变量名 变量名 ++ 右到左 单目运算符
2 自减运算符 – 变量名 变量名 – 右到左 单目运算符
2 * 取值运算符 * 指针变量 右到左 单目运算符
2 & 取地址运算符 & 变量名 右到左 单目运算符
2 ! 逻辑非运算符 ! 表达式 右到左 单目运算符
2 ~ 按位取反运算符 ~ 表达式 右到左 单目运算符
2 sizeof 长度运算符 sizeof(表达式) 右到左
3 / 表达式 / 表达式 左到右 双目运算符
3 * 表达式 * 表达式 左到右 双目运算符
3 % 余数(取模) 整型表达式 % 整型表达式 左到右 双目运算符
4 + 表达式 + 表达式 左到右 双目运算符
4 表达式 - 表达式 左到右 双目运算符
5 << 左移 变量 << 表达式 左到右 双目运算符
5 >> 右移 变量 >> 表达式 左到右 双目运算符
6 > 大于 表达式 > 表达式 左到右 双目运算符
6 >= 大于等于 表达式 >= 表达式 左到右 双目运算符
6 < 小于 表达式 < 表达式 左到右 双目运算符
6 <= 小于等于 表达式 <= 表达式 左到右 双目运算符
7 == 等于 表达式 == 表达式 左到右 双目运算符
7 != 不等于 表达式!= 表达式 左到右 双目运算符
8 & 按位与 表达式 & 表达式 左到右 双目运算符
9 ^ 按位异或 表达式 ^ 表达式 左到右 双目运算符
10 \ 按位或 表达式 \ 表达式 左到右 双目运算符
11 && 逻辑与 表达式 && 表达式 左到右 双目运算符
12 \ \ 逻辑或 表达式 \ \ 表达式 左到右 双目运算符
13 ?: 条件运算符 表达式 1? 表达式 2: 表达式 3 右到左 三目运算符
14 = 赋值运算符 变量 = 表达式 右到左
14 /= 除后赋值 变量 /= 表达式 右到左
14 *= 乘后赋值 变量 *= 表达式 右到左
14 %= 取模后赋值 变量 %= 表达式 右到左
14 += 加后赋值 变量 += 表达式 右到左
14 -= 减后赋值 变量 -= 表达式 右到左
14 <<= 左移后赋值 变量 <<= 表达式 右到左
14 >>= 右移后赋值 变量 >>= 表达式 右到左
14 &= 按位与后赋值 变量 &= 表达式 右到左
14 ^= 按位异或后赋值 变量 ^= 表达式 右到左
14 \ = 按位或后赋值 变量 \ = 表达式 右到左
15 , 逗号运算符 表达式, 表达式,… 左到右

3. 其余

次要是别多写分号,switch 别忘了 break,别写空 else 分支。

第三章 语义“陷阱”

1. 指针与数组

struct {Int p[4];
    Double x;
}b[17];

int calendar[12][31];
int (*p)[31];
sizeof(calendar):12*31=372

calendar[0] // 指向该一维数组,对应 *p
calendar[0][0]
......
calendar[0][30]
calendar[1] // 指向该一维数组,对应 *(p+1)
calendar[1][0]
......
calendar[1][30]
......
......
......
calendar[11] // 指向该一维数组,对应 *(p+11)
calendar[11][0]
......
calendar[11][30]

2. 内存调配

free(r);

用 malloc 显式调配的空间,不会再退出本函数后主动开释掉,而是会等程序员显式开释后才隐没。

留神查看,malloc 调配的内存可能失败。

C 语言中会主动地将作为函数参数的数组申明转换为对应的指针申明,如:

int strlen(char s[]){}等价于 int strlen(char *s){ }
但在其余情景下不会主动转换,也就是说不等价,如:extern char hello[]; 和 extern char *hello; 齐全不同。

边界计算
本人实现一个 memcpy 函数:

void memcpy(char *dest, const char *source, int k)
{while( --k >= 0)
        *dest++ = *source++;
}

重点是:操作时肯定要晓得操作数据的长度。

整数溢出

  • 两个有符号整数相加会产生溢出。
  • 两个无符号整数相加不会产生溢出。
  • 一个有符号和一个无符号整数相加,因为有符号被主动转换成无符号,所以也不会溢出。

第四章 连贯

编译器个别每次只解决一个文件。编译器的责任是把 C 源程序翻译成对连接器有意义的模式。

许多零碎中的连接器是独立于 C 语言实现的,因而如果链接时候谬误起因是与 C 语言相干的,连接器无奈判断谬误起因。但连接器可能了解机器语言和内存布局。

典型的连接器把由汇编器或编译器生成的若干个指标模块,整合成一个被称为载入模块或可执行文件的实体。

连接器通常把指标模块看成是由一组内部对象组成的。每个内部对象代表着机器内存中的某个局部,并通过一个内部名称来辨认。因而,程序中的每个函数和每个内部变量,如果没有被申明为 static,就都是一个内部对象。static 的不会与其它源程序文件中的同名函数或同名变量发生冲突。对于非 satatic 的函数或变量的名称抵触的解决办法将在前面探讨。

除了内部对象外,指标模块中还可能包含了对其余模块中的内部对象的援用,当连接器读入一个指标模块时,它必须解析出这些援用,并作出标记阐明这些内部对象不再是未定义的。

连接器的输出是一组指标模块文件和库文件。输入是一个载入模块。

防止内部变量的函数的抵触和不统一等问题的方法:

每个内部对象只在一个头文件里申明,须要用到该内部对象的所有模块都应该包含这个头文件。

定义该内部对象的模块也应该包含这个头文件。

第五章 库函数

没什么好说的,就是 apue 的一些函数而已。

第六章 预处理器

宏定义:次要是了解 宏不是函数,而是间接替换

  1. 不能漠视宏定义中的空格:

    #define f (x) ((x)-1 ):因为 f 前面多了一个空格,所以 f(x)代表(x) ((x)-1 )
  2. 宏并不是函数,所以留神那些括号:

    #define abs(x) (( (x) >= 0)?(x):-(x) )
    #define max(a,b) ((a)>(b)?(a):(b) )
  3. 宏并不是语句:

    #define assert(e) if (!e) assert_error(__FILE__, __LINE__)
  4. 宏不是类型定义

    • 谬误用法:
    #define int_8_ int*
         int_8 a,b; // 则 a 是指针,b 是 int 型
    • 正确用法:应该用 typedef
    typedef int * int_8_;

第七章 可移植性缺点

次要是:

  1. 应答 C 语言规范的变更;
  2. 标识符名称的限度;
  3. 整数的大小;
  4. 字符是有符号整数还是无符号整数;
  5. 移位运算符;

    1. 在向右移位时,空出的位是由 0 填充还是 1,还是由符号位的正本填充?如果被移位对象是无符号数,那么由 0 填充;如果是有符号数,那么是 0 或符号位的正本。
    2. 移位操作的位数容许的取值范畴是什么?如果被移位对象的长度是 n 位,那么移位计数必须大于或等于 0,而严格小于 n。
  6. 移植性需思考的中央:

    1. 机器的字符表不同。
    2. 有的机器是 one’s complement,有的机器是 two’s complement 的。基于 2 的补码的计算机,所容许示意的从属取值范畴要大于负数取值范畴,所以有时取负值的运算会导致溢出。
    3. 各机器对取模运算的定义不同。

第八章 习用与答案

将习用的 c == '\t' 写作'\t' == c

一旦写错成 = 号,编译器就能查看进去。


欢送关注我的微信公众号【数据库内核】:分享支流开源数据库和存储引擎相干技术。

题目 网址
GitHub https://dbkernel.github.io
知乎 https://www.zhihu.com/people/…
思否(SegmentFault) https://segmentfault.com/u/db…
掘金 https://juejin.im/user/5e9d3e…
开源中国(oschina) https://my.oschina.net/dbkernel
博客园(cnblogs) https://www.cnblogs.com/dbkernel
正文完
 0