【C/C++】《C陷阱与缺陷》阅读笔记
C缺陷与陷阱
前言
尝试使用新的读书笔记记录方法,只针对关键提示性语句进行记录,关于详细的概念知识点记录在后,仅在忘记时再去查看
第0章 导读
无
第1章 词法“陷阱”
=
不同于==
&
和|
不同于&&
和||
词法分析的贪心法 见代码清单1-1
整形常量 0
开头为八进制,0x
开头为十六进制
单引号和双引号 见代码清单1-2
// 代码清单 1-1
int a = 5, b = 4;
int c = a---b; // 等价于 a-- - b;
// 此时 c = 1, a = 4, b = 4
// 代码清单 1-2
char ch = 'a'; // 单引号实际上表示的是一个整数
char *str = "abcdefg"; // 双引号实际上表示的是一个地址,存储着abcdefg和\0
第2章 语法“陷阱”
理解函数声明 见代码清单2-1
运算符的优先级问题 见代码清单2-2
注意分号 见代码清单2-3
注意switch
的break
函数与括号 见代码清单2-4
“悬挂”else 见代码清单2-5
// 代码清单 2-1
(*(void (*)())0)();
// 如果知道了如何声明一个类型的变量,那么该类型的类型转换符就很容易得到
void (*pf)(); // 函数指针,指向参数列表为空,返回值为void的函数。
(void (*)()) pf; // 它对应的类型转换符,可以将值转换为之前那种函数指针
(*pf)(); // 使用函数指针调用函数,因为()的优先级高于*,所以需要带括号
// 所以最初的语句意思为:调用0位置对应的一个函数,其参数与返回值都为空
// 代码清单 2-2
if (flags & FLAG != 0) ... // 本意是判断flags与FLAG按位与的结果是否为0,结果由于!=优先级高于&,发生了错误
r = hi<<4 + low; // 本意是将hi的后四位作为r的前四位,low的后四位作为r的后四位
// 结果由于+的优先级高于<<,成为了这样 hi << (4+low)
// 解决方案是可以加括号,或者将+改为| r = hi << 4 | low;
// 优先级表 C语言
() [] -> . // 自左向右 不算是真正意义上的运算符,数组下标、函数调用、结构成员
! ~ ++ -- - (type) * & sizeof // 自右向左 单目运算符
* / % // 自左向右 双目运算符
+ - // 自左向右
<< >> // 自左向右 移位运算符
< <= > >= // 自左向右 关系运算符
== != // 自左向右
& // 自左向右 按位运算符
^ // 自左向右
| // 自左向右
&& // 自左向右 逻辑运算符
|| // 自左向右
?: // 自右向左 条件运算符
assignments // 自右向左 赋值运算符
, // 自左向右 逗号运算符
// 重要的两点:逻辑运算符低于关系运算符、移位运算符低于双目运算符,高于关系运算符
// 代码清单 2-3
// 1.多了分号
if (x > 5); // 这种情况下,不管x的值是多少,x=5都会执行
x = 5;
// 2.少了分号
if (x < 3) // 这种情况下,如果函数返回值是void,会提示错误
return // 如果没有显式写出返回类型,可能会将x=5的结果作为返回值
x = 5;
// 代码清单 2-4
void func(); // 函数声明
func(); // 调用函数,整个表达式的值为函数的返回值
func; // 函数地址
// 代码清单 2-5
if (x == 0) // 由于 else 始终与同一对括号内最近的 if 相结合
if (y == 0) error(); // 所以这里的 else 匹配的是 if (y == 0)
else { // 与本意不符,应该改为下面的样子
z = x + y;
}
if (x == 0) {
if (y == 0) error();
} else {
z = x + y;
}
第3章 语义“陷阱”
数组与指针 见代码清单3-1
非数组的指针 动态内存分配 见代码清单3-2
作为参数的数组,会被自动转换为相应的指针
复制指针并不同时复制指向的数据
空指针不要去试图访问目标内存
边界计算与不对称边界 见代码清单3-3
求值顺序 见代码清单3-4
&&
、||
和!
与 &
|
~
^
整数溢出 见代码清单3-5
为main提供返回值
// 代码清单 3-1
// 1. C语言中只有一维数组,多维数组只是元素为一维数组的一维数组
// 2. 对于数组,只能做两件事,确定数组的大小,获取指向第一个元素的指针
int a[10];
a[3]; // a[3] 实际上是 *(a+3)
// 给指针加上一个整数,与给指针的二进制表示加上同样的整数,含义截然不同
sizeof(a); // 10 * sizeof(int)
// 数组除了被用于sizeof的参数这一情形,在其他情况下,都表示指向首元素的指针
int b[10][20];
int (*p)[20]; // 数组指针
int * p[20]; // 指针数组
// 代码清单 3-2
char s[] = "hello ";
char t[] = "world!";
char *r;
r = (char *) malloc(strlen(s) + strlen(t) + 1);
// 字符串后面有个\0
if (!r) { // 要判断是否成功分配了空间
/* do something */
}
strcpy(r, s);
strcat(r, t);
/* do something */
free(r); // 释放空间
// 代码清单 3-3
for (i = 0; i < 10; i++) {...} // 用第一个入界点和第一个出界点来表示一个数值范围
int arr[10]; // ANSI C标准明确允许这种用法,
int *ptr; // 数组中实际不存在的“溢界”元素的地址位于数组所占内存之后,
for (ptr = arr; ptr != &arr[10]; ptr++) // 这个地址可以用于赋值和比较
... // 如果要引用该元素就是非法的了
// 代码清单 3-4
if (c != 0 && a / c > 3) // 即使 c 为0也不会出现除0错误
...
// C语言中只有四个运算符存在规定的求值顺序
left && right // && 仅在左侧为true的时候才会对右侧求值
left || right // || 仅在左侧为false的时候才会对右侧求值
a ? b : c // ?: 在a为真时对b求值,否则对c求值
a, b, c // , 首先对左侧求值,然后丢弃,再对右侧求值
// 其他所有运算符对其操作数求值的顺序是未定义的
// 代码清单 3-5
// C语言中有两类整数算术运算,有符号运算与无符号运算
// 无符号运算中,不存在“溢出”概念,所有结果都是以2的n次方为模
// 发生“溢出”时,所有关于结果如何的假设都不再可靠
if ((unsigned)a + (unsigned)b > INT_MAX) // 一种判断溢出的方法,转换为无符号
if (a > INT_MAX - b) // 另一种判断溢出的方法
第4章 连接
连接器的概念 见代码清单4-1
声明与定义(外部链接性) 见代码清单4-2
命名冲突与static修饰符(内部链接性) 见代码清单4-3
形参、实参和返回值
检查外部类型 见代码清单4-4
头文件中声明外部对象,在具体文件中进行初始化
// 代码清单 4-1
// 典型的连接器把由编译器或汇编器生成的若干个目标模块,
// 整合成一个被称为载入模块或可执行文件的实体,
// 该实体能够被操作系统直接执行
// 代码清单 4-2
int a; // 外部链接性静态变量
extern int a; // 显式说明使用在其他文件中定义的a
// 代码清单 4-3
static int a; // 内部链接性静态变量,只在当前源文件中有效
static int g(); // 也可应用于函数
// 代码清单 4-4
char filename[] = "/etc/passwd"; // A文件中定义变量
extern char filename[]; // B文件中使用,可以
extern char * filename; // C文件中使用,不可以,尽管实际上是指针,但是意义是不同的
第5章 库函数
尽量使用系统头文件
getchar()
函数返回的是int
类型
如果要同时进行输入和输出,必须在其中插入fseek()
函数调用
缓冲输出与内存分配 见代码清单5-1
使用error
检查错误时,应先确认程序执行是否真的失败
signal
可以捕获异步事件(不懂,暂时跳过)
// 代码清单 5-1
char buf[BUFSIZ]; // 错误的使用,因为这种情况下,在缓冲区清空之前,那片内存可能被释放
setbuf(stdout, buf);
static char buf[BUFSIZ]; // 第一种正确方法,目标内存持续时间与程序一样长
setbuf(stdout, (char *) malloc(BUFSIZ)); // 第二种正确方法,动态分配缓存区
第6章 预处理器
预处理器的重要性 见代码清单6-1
不能忽视宏定义中的空格 见代码清单6-2
宏并不是函数 见代码清单6-3
宏并不是语句 见代码清单6-4
宏并不是类型定义 见代码清单6-5
// 代码清单 6-1
#define max_size 5000 // 预处理器可以将某个特定值在程序中出现的所有实例一次修改
#define my_max(a, b) ((a) > (b) ? (a) : (b)) // 预处理器可以写类似于函数的块,但是却没有函数调用的开销
// 代码清单 6-2
#define f (x) ((x) - 1) // 错误
#define f(x) ((x) - 1) // 正确
// 代码清单 6-3
#define abs(x) (((x) >= 0) ? (x) : -(x)) // 注意宏定义中出现的所有括号
// 它们的作用是预防引起与优先级有关的问题
i = 0;
cout << abs(i--) << endl; // 显示结果为 -1 ,与预期不符
// 因为如果一个操作数被多次用到,就会被求值多次,而函数不会
cout << abs(15 * abs(3)) << endl; // 结果正确,但是展开过程中产生了比较庞大的表达式
// 操作数使用越多,表达式yeu
// 代码清单 6-4
assert(x > y); // assert 参数是一个表达式,如果为0,就终止程序,给出出错信息
#define assert(e)\ // 第一次尝试定义 assert 宏
if (!e) assert_error(__FILE__, __LINE__)
if (x > 0 && y > 0) // 这种情况下,展开时,
assert(x > y); // else语句会自动匹配到assert里面的if语句
else // 所以之前的定义是不对的
assert(y > x);
#define assert(e)\ // 正确的定义类似于一个表达式
((void)((e)||assert_error(__FILE__,__LINE__)))
// 代码清单 6-5
#define T1 int * // #define 定义类型
typedef int * T2 // typedef 定义类型
T1 a, b; // a是int指针 b是int变量
T2 c, d; // c、d都是int指针
第7章 可移植性缺陷
C语言标准变更 (示例标准太老,跳过)
标识符名称的长度与大小写
整数的大小 见代码清单7-1
字符的符号性 见代码清单7-2
移位运算符 见代码清单7-3
内存位置为0,即NULL只可用于赋值或比较
除法运算时发生的截断 见代码清单7-4
随机数的最大值是RAND_MAX
大小写转换 见代码清单7-5
部分系统中,某块内存释放后会保留一段时间
// 代码清单 7-1
// 1. 三种类型的整数其长度是非递减的,也就是说short小于等于int, int小于等于long
// 2. 一个普通(int类型)整数足够大以容纳任何数组下标
// 3. 字符长度由硬件特征决定
typedef long tenmil; // 可以使用typedef声明一个想要的类型,如果大的话只修改本条声明即可
// 代码清单 7-2
// 对于一般的字符变量(char类型),不同的编译器处理不同,有可能是有符号,也可能是无符号
unsigned char ch; // 可以声明精确的字符类型
char ch;
(unsigned) ch; // 无效,因为首先将ch转换为int,这个过程不可控制
(unsigned char) ch; // 有效,直接将ch从char转换为unsigned char
// 代码清单 7-3
// 1. 向右移位时,使用符号位填充还是使用0填充,取决于实现,无符号数一定使用0填充
// 2. 移位运算符允许的取值范围在[0, n)中
// 代码清单 7-4
// C语言中遵守的规则
// 1. 除数 x 商 + 余数 = 被除数
// 2. 如果被除数为负数,商和余数的符号会发生改变,但是绝对值不变
int a1 = 10, b1 = 3;
int c1 = a1 / b1; // 3
int d1 = a1 % b1; // 1
int a2 = -10, b2 = 3;
int c2 = a2 / b2; // -3
int d2 = a2 % b2; // -1
// 代码清单 7-5
// 最初的toupper()和tolower()被实现为宏
#define toupper(c) ((c)+'A'-'a')
#define tolower(c) ((c)+'a'-'A')
// 考虑到对于无效参数会返回无效值,又更改为函数
int toupper(int c)
{
if (c >= 'a' && c <= 'z')
return c + 'A' - 'a';
return c;
}
// 考虑到额外开销,又重新引入宏,不过修改了宏名
#define _toupper(c) ((c)+'A'-'a')