码迷,mamicode.com
首页 > 其他好文 > 详细

C陷阱与缺陷学习笔记

时间:2016-04-23 11:57:41      阅读:255      评论:0      收藏:0      [点我收藏+]

标签:

本书的介绍

作者以自己1985年在Bell实验室时发表的一篇论文为基础,结合自己的工作经验扩展成为这本对C程序员具有珍贵价值的经典著作。写作本书的出发点不是要批判C语言,而是要帮助C程序员绕过编程过程中的陷阱和障碍。
全书分为8章,分别从词法分析、语法语义、连接、库函数、预处理器、可移植性缺陷等几个方面分析了C编程中可能遇到的问题。最后,作者用一章的篇幅给出了若干具有实用价值的建议。
本书适合有一定经验的C程序员阅读学习,即便你是C编程高手,本书也应该成为你的案头必备书籍。

前言

N年读过这本书,当时读的时候囫囵吞枣,加上时间久远,90%的内容都忘记了。昨天在整理书籍的时候翻出来了,这本书短小精悍却不失为经典之作。抽出时间再拜读一遍,顺便做做笔记,记录精华。

第一章,词法“陷阱”

1.1 =不同于==

c语言中”=”是赋值运算符
“==”是关系运算符,用于两个数进行比较

1.2 & 和 | 不同于&& 和 ||

&:按位与,优先级高于&&
|:按位或,优先级高于||
&&:逻辑与
||:逻辑或

1.3 词法分析中的“贪心法”

原文是这么说的:如果编译器输入流截止某个字符之前都已经被分解为一个个符号,那么下一个符号将包括从该字符之后可能组成一个符号的最长字符串。
例子:==被编译器解析为一个比较符号,而= =被编译器解释为两个赋值符号
a=b/*p;本意是a=b/(*p),但*被编译器解释为注释的开始符号,要么写成a=b/ *p或者a=b/(*p)

1.4 整型常量

在C/C++中,表示8进制整数需要在最前面加0,如0122
在表示十进制的地方,不要用0进行文本格式的对齐。

1.5 字符与字符串

单引号中的字符被编译器视为一个整数,不管单引号中有几个字符,比如’a’和’abcd’都被视为整数。

unsigned int value1 = ‘tag1‘;   
unsigned int value2 = ‘cd‘;   
char value3 = ‘abcd‘;           

在vs2013编译上述代码后各个变量结果如下:

value1= ‘t‘<<24 | ‘a‘<<16 | ‘g‘<<8 | ‘1‘=0x74616731
 value2= ‘c‘<<8 | ‘d‘=0x00006364
 value3=‘d‘=0x64

可以用多个字符给value3赋值,但value3仅保留最后一个字符的值。
对于value1和value2,赋值时最多传入四个字符,可以少于4个字符,否则编译器会报错。

第2章 语法“陷阱”

2.1 理解函数声明

这一节主要讲的是函数指针的声明、定义和使用。
void (*pFunc)();
typedef void (*pFunc)(); 相当于定义了一个数据类型pFunc,这个数据类型是函数指针类型,这个类型是void (*)();
pFunc f;
使用f的方式如下:
(*f)(); ANSI C标准允许进行简写,简化为 f();
注意,上面的调用方式不能写成下面的形式:
*f(); 由于括号的优先级最高,所以这种方式实际变成*(f());
*f; 只计算出一个整数值,并未进行函数调用;
f; 只计算出一个整数值,并未进行函数调用;

2.2 运算符的优先级问题

在编程中要特别注意运算符的优先级。
运算符的优先级可以利用口诀进行记忆,即“单算移关与,异或逻条赋”
■“单”表示单目运算符:逻辑非(!),按位取反(~),自增(++),自减(–),取地址(&),取值(*);
■“算”表示算术运算符:乘、除和求余(*,/,%)级别高于加减(+,-);
■“移”表示按位左移(<<)和位右移(>>);
■“关”表示关系运算符:大小关系(>,>=,<,<=)级别高于相等不相等关系(==,!=);
■“与”表示按位与(&);
■“异”表示按位异或(^);
■“或”表示按位或(|);
■“逻”表示逻辑运算符:逻辑与(&&)级别高于逻辑或(||);
■“条”表示条件运算符(? :);
■“赋”表示赋值运算符(=,+=,-=,*=,/=,%=,>>=,<<=,&=,^=, |=,!=);
◆逗号运算符(,) 级别最低,口诀中没有表述,需另加记忆…
◆(),[],->这些其实不算运算符,级别最高

2.3 注意作为语句结束标志的分号

在大多数情况下,多写一个分号编译器并不会报警,并且程序能正常运行;漏写分号编译时会出错。
但是,这可能导致严重的bug。
比如:

多分号的情况

if(x==y);
   a=b;

这样a=b;不会受if语句的影响。

缺少分号的情况

if(x==y)
   return
x=5;

如果return后面没有分号,返回值将从void变成整数1,大多数情况下会被编译器检测到,如果函数的返回值恰好是整型,这将导致非常严重的bug。

struct ab{
int x;
int y;
}
main()
{}

struct结构体定义结束时缺少分号,main函数的返回值就变成了结构体类型。

2.4 switch语句

switch中的case语句结束时不要忘记break语句,确实不需要break语句的时候应明确标注,方便代码维护。
笔者初学C语言时曾经在这个问题上栽过跟头,一段代码运行结果与预期不符,调试三天最后发现问题在于漏写了break;

2.5 函数调用

2.6 “悬挂”else引发的问题

else与最近的if进行匹配。
例如:
if(x==y)
if(a==b)
printf(“a==b”);
else
printf(“x!=y”);

if(x==y)
{
  if(a==b)
printf("a==b");
}
else
 printf("x!=y");

执行结果将截然不同。

第3章 语义“陷阱”

3.1 指针与数组

  • C语言中只有一维数组,多维数组实际上也是用一维数组表示。
  • 利用数组下标进行运算等同于对应的指针运算。a[i]与i[a]都能通过编译并且正常运行。p=&a[0]与p=a是一样的。
  • 除了a被用作运算符sizeof这一情形,在其它所有的情形中数组名都代表指向数组a中下标为0的元素的指针。

3.2 非数组的指针

char *r,*s,*t;
r=malloc(strlen(s)+strlen(t));
strcpy(r,s);
strcat(r,t);

这段代码有三处错误:
1. malloc之后未检查返回值是否为NULL
2. 注意sizeof与strlen的区别,strlen计算字符串所包括的字符数目,不包含结尾的’\0’字符,所以分配内存时应该是strlen(s)+1;
3. 分配完的内存应及时释放,避免发生内存泄漏

3.3 作为参数的数组声明

C语言中会自动将形参中作为参数的数组声明转化为对应的指针声明。

3.4 避免“举隅法”

char *p,*q;
p="xyz";
q=p;

在这个例子中,p和q指向了同一块内存,复制指针并不同时复制指针指向的数据。

3.5 空指针并非空字符串

编程时应注意空指针和空字符串的区别。
比如:

char *p,*q;
p=NULL;
q=malloc(10);
\*q=0;

在这个例子中,p就是空指针,q不是空指针,q指向了空字符串。
可以使用下面的代码判断是否是空字符串:

char *p;
if(p!=NULL && *p)
{}

3.6 边界计算与不对称边界

c语言中数组的下标是从0开始的。
在循环中利用操作数组元素非常容易出现越界问题,需要特别注意。

3.7 求值顺序

在C语言中,定义了||、&&、?:、, 的求值顺序。

C语言规定,必须首先对左侧操作数求值,然后根据需要对右侧操作数求值;
根据需要利用了短路性质,即:
A&&B, 当A为false时,不去计算B的值而直接返回false;当A为true时,计算B的值。
A || B,当A为true时, 不去计算B的值而直接返回true;当A为false时,计算B的值。
比如:

if(count!=0 && (a/count)>2)
{}

先计算左侧的表达式判断count是否等于0,count不为0才计算a/count。在这个例子中,求值顺序至关重要,否则会引发除0异常。
在比如一个例子:

char *p;
if(p!=NULL && *p)
{}

首先判断p是否是空指针,然后判断是否是空字符串。

对于?:这个条件运算符,比如:a?b:c

操作数a先被求值,根据a的值再求操作数b或者操作数c的值,只计算其中一个值。

逗号运算符,首先对左侧操作数求值,然后该值被”丢弃”,在对右侧操作数求值。

C语言中其他所有运算符对其操作数求值的顺序是未定义的。特别指出,赋值运算符并不保证任何求值顺序。
例如:

i=0;
while(i<n)
{
    y[i++]=x[i];
}

在这个例子中,左侧操作数中的i++有可能比x[i]先执行,也有可能比x[i]后执行。

3.8 运算符&&、|| 和 !

注意逻辑与和位与、逻辑或和位或的区别。
逻辑运算有短路性质,而位运算则没有此性质。

3.9 整数溢出

在进行算术运算时,应考虑整数溢出的影响。如果溢出对结果有影响,就应该进行判断。
判断的方法是:

//把a,b两个数均视为无符号数
if((unsigned int)a+(unsigned int)b>INT_MAX)
{}

limits.h头文件中有相关的宏定义:

#define INT_MIN     (-2147483647 - 1) /* minimum (signed) int value */
#define INT_MAX       2147483647    /* maximum (signed) int value */

3.10 为函数main提供返回值

main函数如果未声明返回值,默认返回整数值。这个返回值用来告知操作系统执行结果,返回0表示成功,返回非0表示失败。
大部分情况下这样做没有问题。但如果系统关注这个执行结果,就必须明确返回一个有意义的值。
第4章 连接
4.1 什么是连接器
4.2 声明与定义
4.3 命名冲突与static修饰符
4.4 形参、实参与返回值
4.5 检查外部类型
4.6 头文件
第5章 库函数
5.1 返回整数的getchar函数
5.2 更新顺序文件
5.3 缓冲输出与内存分配
5.4 使用errno检测错误
5.5 库函数signal
第6章 预处理器
6.1 不能忽视宏定义中的空格
6.2 宏并不是函数
6.3 宏并不是语句
6.4 宏并不是类型定义
第7章 可移植性缺陷
7.1 应对C语言标准变更
7.2 标识符名称的限制
7.3 整数的大小
7.4 字符是有符号整数还是无符号整数
7.5 移位运算符
7.6 内存位置0
7.7 除法运算时发生的截断
7.8 随机数的大小
7.9 大小写转换
7.10 首先释放,然后重新分配
7.11 可移植性问题的一个例子
第8章 建议与答案
8.1 建议
8.2 答案

C陷阱与缺陷学习笔记

标签:

原文地址:http://blog.csdn.net/zfs2008zfs/article/details/51224789

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!