第一章 词法“陷阱”
1.1 =和==的区别及注意事项
=是赋值运算,而==是条件判断符号,很多程序员在条件判断时,往往容易把==写成=号,例如:
while(c = ’ ‘||c == ‘\t’ || c ==’\n’)
c=getc(f);
此时,因为赋值运算=的优先级低于逻辑运算符号,实际是将以下表达式的赋给c:
’ ‘|| c == ‘\t’ || c == ‘\n’
因为’ ‘不等于0,所以该循环会一直进行下去直到整个文件结束
1.2 & 、| 和&& 、||
&、|是按位运算,而&& 、|| 是逻辑运算,值得注意的是用&&做逻辑运算时,从左往右运算,只要有一个条件为0立即判断整个条件判断为假,而不再进行后面的条件判断,|| 也是从左往右运算,只要有一个条件为真,就不再进行后面的条件判断,立即返回真。
1.3 贪心法
例:
a—b 与 a – -b 相同
注意:/和会匹配为注释符号/,表示一段注释的开始,而编译器会将/之后都当做注释直到遇到/,使用/与*时要注意。
1.4 整型常量
0开头为八进制,0x开头为十六进制,系统默认为十进制。
1.5 字符与字符串
C语言中用单引号引起的一个字符代表一个整数,而用双引号引起的字符串代表的是指向无名数组起始字符的指针。
例如:
char*p=’/’;
编译会生成一个错误信息,因为’/’并不是一个字符指针
应改成char p[]={‘\’};
第二章 语法“陷阱”
2.1 理解函数声明
例:
(void(*)())0 表示将常数0转型为“指向返回值为void的函数的指针”类型
2.2 C语言运算符优先级表
优先级表链接:链接文本
记住两点:
1.任何一个逻辑运算符的优先级低于任何一个关系运算符。
2.移位运算符的优先级比算术运算符要低,但是比关系运算符要高。
2.3 注意作为语句结束标志的分号
例:
if(NULL != p);
fun();
这个代码等效于
if(NULL != p)
{
;
}
fun();
本来是想当NULL != P 的时候被调用,而多写一个分号之后在任何时候都被调用
因此在用if的时候,要注意,多写一个分号将导致结果与预想的差很远。
2.4 switch、case组合
有两个规则:
1.每个case语句的结尾绝对不要忘了加break,否则将导致多个分支重叠(除非有意使多个分支重叠)
2.最后必须使用default分支。
记住:
case后面只能是整型或字符型的常量或常量表达式。
第三章 语义“陷阱”
3.1指针和数组
int *p;
指针和数组的示意图
指针p在32位系统下,sizeof(p)的值为4,p本身有自己的起始地址,而p有4个字节大小的空间来存储某个内存地址,也就是p所指向的内存
int a[5];
当我们定义一个数组时,编译器根据指定的元素个数和元素类型分配一块确定大小的内存,在32位系统下
区别:&a[0]是数组a首元素的首地址,而&a是数组a的首地址
数组作为参数时,C语言会自动地将作为参数的数组声明转换为相应的指针声明。
例如:int strlen(char s[])
{
/具体内容/
}
与
int strlen(char *s)
{
/具体内容/
}
写法完全相同
3.4 避免“举隅法”
例:
char *p,*q;
p=”xyz”;
q=p;
分析:p和q指向内存中同一块地址,复制指针并不同时复制指针指向的数据。
3.5 空指针并非空字符串
将0赋值给一个指针变量时,绝不能企图使用该指针所指向的内存中的内容。
3.8 运算符 &&、|| 和!
这三个都是逻辑运算符,对操作数处理方式视为要么“真”,要么“假”。
3.9整数溢出
C语言有两类整数算术运算,有符号运算要考虑溢出的情况,无符号无“溢出”说法。
3.10 为函数main提供返回值
典型处理方案:返回值为0代表程序执行成功,返回值非0表示程序执行失败。
第四章 连接
4.2 声明与定义
声明:对已经存在空间说明使用。
l例如:extern int a; 说明a是一个外部整型变量。
定义:分配内存动作发生。
例如:int a=7; 定义的同时也确定了a的初始值。
4.3 命名冲突与static修饰符
static 能减少命名冲突。
static有两个作用:
1.修饰变量(局部变量和全局变量)
静态局部变量,在函数体里面定义,值只被初始化一次,生存周期比本身所在函数长,也就是这个函数运行结束,这个静态变量的值也不会被销毁。
静态全局变量:作用域限于变量被定义的文件中,其他文件即使用extern声明也无法使用它。
2.修饰函数
静态函数:对函数的作用域仅局限于本文件。
4.4 形参、实参与返回值
一个函数在被定义或声明之前被调用,那么默认返回值int类型。调用函数时,要先声明再调用。
4.5 检查外部类型
保证一个特定名称的所有外部定义在每个目标模块中都有相同的类型。
例:
在一个文件中包含定义:char filename[]=”/etc/passwd”; // 文件一
而在另一个文件中包含声明:extern char* filename; //文件二
更正:
把文件一改成:char *filename=”/etc/passwd”;
或者把文件二改成:extern char filename[];
4.6头文件
格式1:
include
filename为要包含的文件名称,用尖括号括起来,表示预处理到系统规定的路径中去获得这个文件。找到文件后,用文件内容替换该语句。
格式2:#include”filename”
filename为要包含的文件名称,双引号表示预处理应该在当前目录中查找文件名为“filename”的文件,若没有找到,则按系统指定的路径信息搜索其他目录。找到文件后,用文件内容替换该语句。
第五章 库函数
5.1 返回整数的getchar函数
例:
main()
{
char c;
while((c = getchar()) != EOF)
putchar(c);
}
编译在表达式中不是比较c与EOF,而是比较getchar函数的返回值(整数)与EOF。
5.2 更新顺序文件
在对同一个文件,同时进行读和写操作时,要记得用fseek偏移到相应的文件操作位置。
5.3 缓冲输出与内存分配
程序输出两种方式:1、及时处理 2、把数据缓存起来,再大块写入。
setbuf库函数的作用是把字符缓冲到一个buf中,直到buf缓冲区满或程序员直接调用fflush才输出字符。
避免缓冲区被提前释放的方法:1、声明buf数组为静态数组 2、动态分配缓冲区
5.4 使用errno检测错误
使用格式:
/* 调用库函数*/
if(返回的错误值)
检查errno
5.5库函数signal
头文件 #include
define f (x) ((x)-1)
因为f 和(x)之间多了个空格符号,所以f等价于(x) ((x)-1)
如果希望定义f(x) 为((x)-1)
应写成 #define f(x) ((x)-1)
6.2 宏并不是函数
函数调用比宏实现带来更多的系统开销
6.4 宏不是类型定义
例:
define T1 struct foo*
typedef struct foo *T2;
当我们用上面两个定义来声明多个变量时
T1 a,b;
T2 a,b;
第一个声明被扩展为
struct foo *a,b;
语句a被定义为一个指向结构体的指针,而b却被定义为一个结构体(而不是指针)
第二个声明则都定义了a和b为指向结构体的指针。
因此从上面例子看到,宏定义不是类型定义。
第七章 可移植性缺陷
7.2 标识符名称的限制
ANSI C标准只能保证,C实现必须能够区别出前6个字符不同的外部名称。(没有区分大小写字母)
为保证程序的可移植性,要谨慎地选择外部标识符的名称
例:State 与STATE print_fileds 与 printf_float 不恰当
7.3 整数大小
C语言三种整数:short型、int型和long型。
short型能被int型容纳,int型能被long型容纳,int型整数能容纳任何数组下标。
7.4 字符是有符号整数还是无符号整数
正确处理方式:用(unsigned char)c 直接转换
7.5 移位运算符
左移时,在后面的位补0,而右移时,如果是无符号数,空位用0填充,如果是有符号数,可以这个数声明为无符号类型再右移。
移位的范围: [ 0, 被移位的对象长度(n) ) 即大于或等于0 小于 n
7.6 内存位置0
NULL 指针不指向任何对象,除非是用于赋值或比较运算,除此之外使用NULL指针都是非法的。
7.7除法运算时发生的截断
避免截断的最好做法是:声明变量为unsigned 类型的数。
7.8 随机数的大小
rand()函数最大值等于ANSI C 标准中的一个常数RAND_MAX
7.9 大小写转换
库函数中小写转大写 :toupper() 大写转小写tolower
7.10 先释放,然后重新分配
动态分配问题
C语言提供了3个内存分配函数:malloc,realloc和free。
malloc()动态分配内存,分配成功返回void型指向新分配的内存的指针,分配失败返回NULL,free配合malloc()使用释放申请的内存。必须用if(NULL != p) 语句来验证内存确实分配成功了。
void *realloc (void *ptr, size_t new_size );函数调用时,可调整(扩大或缩小)这块内存区域为新的大小。
动态分配需要考虑的问题—-内存泄露
内存泄露产生的原因:由malloc或者new分配的内存,用完之后没有及时free或delete,这块内存就无法释放,直到整个程序终止。
解决办法:每次用完动态分配的内存后,及时free或delete掉。内存释放完后,要及时把指针p置为NULL,以避免野指针的产生。
课后练习题集
练习1-3 为什么n–>0的含义是n– >0而不是n- ->0?
“大嘴法”规则,在编译器读入>之前,就已经将–作为单个符号。
练习1-4 a+++++b的含义是什么?
a ++ ++ +b 但从语法上来说是不正确的
练习3-3 编写一个函数,对一个已排序的整数表执行二分查找。函数的输入包括一个指向表头的指针,表中的元素个数,以及待查找的数值。函数的输出是一个指向满足查找要求的元素的指针,当为查找到要求的数值时,输出一个NULL指针
采用对称边界来写:
int *bsearch(int *p,int n,int value)
{
int low = 0,high = n - 1;
while(low <= high)
{
int mid = (high + low) / 2;
if(value < p[mid])
high = mid -1;
else if(value > p[mid])
low = mid + 1;
else
return p + mid;
}
return NULL;
}
练习5-1 当一个程序异常终止时,程序输出的最后几行常常会丢失,原因是什么?我们能够采取怎样的措施来解决这个问题?
原因:一个异常终止的程序可能没有机会来清空其输出缓冲区就终止了。
措施:强制不允许对输出缓冲。
setbuf(stdout,(char *)0); 在任何输出被写到stdout之前调用。
练习6-2 本章第一节提到的“表达式” (x) ((x)-1) 能否成为一个合格的C表达式?
能。一种是 x是类型名
例:typedef int x;
(x) ((x)-1) 就等价于 (int) ((int)-1)
另一种是 x是函数指针: typedef void (T)(void );
T x;