【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
注意switchbreak
函数与括号 见代码清单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')
posted @ 2020-10-21 12:58  by-sknight  阅读(508)  评论(0编辑  收藏  举报