第一章——词法“陷阱”
-
=
是赋值运算符,==
是比较运算符;=
赋值运算符的优先级要低于逻辑运算符||
,!=
和==
的优先级要高于&
运算符,==
比较运算符的结果只可能是0
或者1
,不可能小于0
-
字符是用单引号引起的实际代表一个整数,整数值对应于该字符在编译器采用的字符集的序列号如
ASCII
码;字符串是用双引号引起的实际代表的是一个指向无名数组的起始字符的指针,该数组被双引号之间的字符以及一个'\0'
初始化。比如:'yes'
表示一个整数值,由y
、e
、s
所代表的整数值按照特定的编译器实现中定义的方式组合得到;"yes"
所代表的含义是y
、e
、s
以及空字符\0
的4个连续内存单元的首地址。两者不能混用。 -
测试程序确定C编译器是否允许嵌套注释:(C语言定义并不允许嵌套注释)
/* /* /0 */ * */ 1 /* / */ 0* /* */ 1
如果编译器允许嵌套注释,两个
/*
和两个*/
符号正好匹配,所以式子结果就是1
;如果编译器不允许嵌套注释,注释中的/*
会被忽略,/
出现在注释中也没有特殊含义,所以式子结果就是0*1
,也就是0。 -
贪心法
n-->0
的含义是n-- >0
而不是n- ->0
,根据贪心法编译器读入>
之前n--
已经是一个整体了a+++++b
的含义是((a++)++)+b
,但语法是错误的,a++
的结果不能作为左值再进行运算,因此编译器不会接受a++
作为后面的++
运算符的操作数。
第二章——语法“陷阱”
-
运算符优先级
*p++
先进行++
运算再进行指针运算任何一个逻辑运算符的优先级低于任何一个关系运算符
移位运算符的优先级比算术运算符要低,但是比关系运算符要高
比如
mid = lo + (hi + lo) >> 1
,先执行加法再执行位移所有的运算符中,逗号运算符的优先级最低
-
分号是语句结束标志
if( STATUS_SUCCESS != (s = foo( arg1, arg2, arg3))); do something
这种例子,尤其是在args很多的时候,还真有可能忘了这是一条if语句而犯了上面这个错误。同理,如果这个if是while,也很有可能犯同样的错误。尤其注意多写分号不会提示任何警告信息的情况
main
函数的返回值类型缺省会自定义为int
类型,返回值为0
代表程序执行成功,返回值是非0
则表示程序执行失败。 -
switch
语句中case
后面的break
根据需要可以进行省却比如实现一个编译器在查找符号时候跳过程序中的空白字符,空格键、制表符和换行符处理都是相同的,除了遇到换行符时程序的代码行计数器进行递增
case '\n': linecount++; //此处没有break语句 case '\t': case ' ': ......
-
函数调用
f()
;//调用函数f
;//计算函数f
的地址,但不调用
第三章——语义“陷阱”
-
数组与指针
对于一个数组,利用数组性质只能确定该数组大小以及获取指向该数组下标为0的元素的指针。其他关于数组的操作本质上都是通过指针进行的
int a[12][30]
该数组拥有12个数组类型的元素,其中每个元素都是一个拥有30个整型元素的数组 -
非数组指针
char *r, *malloc(); r = malloc(strlen(s) + strlen(t) + 1);//字符串结束符要占1位 if(!r) { printf("malloc error\n"); exit(1); } strcpy(r, s); strcat(r, t); free(r);//动态内存使用完及时释放
-
作为参数的数组声明
在函数中无法将一个数组作为函数参数传递,数组名会被转换成指向该数组的第一个元素的指针
int main(int argc, char* argv[]); int main(int argc, char** argv);//两种写法完全等价
-
空指针并非空字符串
当将
0
赋值给一个指针变量时,绝对不会企图使用该指针所指向的内存中的内容if(p == (char*) 0) //合法 if(strcmp(p, (char*) 0) == 0)//非法,strcmp实现会查看它的指针参数所指向内存中的内容的操作
-
边界计算与不对称边界
C语言中拥有
n
个元素的数组,却不存在下标为n
的元素。下界是入界点,包括在取值范围内;上界是出界点,不包括在取值范围之中。比如n
个元素的数组中,0
是数组下标的第一个入界点,n
是数组下标的第一个出界点,元素个数就是出界点和入界点的差值。即区间[a,b)
中有b-a
个元素。设置指针时一般让它指向缓冲区中第一个未占用的字符,大多数情况我们不需要引用该位置的元素,但是需要引用这个位置的元素地址
-
求值顺序
逗号运算符,首先对左侧操作数求值,然后该值被丢弃,再对右侧操作数求值
运算符
&&
和||
在左侧操作数的值能够确定最终结果时根本不会对右侧操作数求值i = 0; while( i < n ) y[ i ] = x[ i++ ];
由于没有说明到底是先算左边还是先算右边,所以可能左边用
y[ i+1 ]
前的结果接收了右边x[ i++ ]
后的结果。当然,也可能左边用y[ i+1 ]
的结果接收右边x[ i++ ]
后的结果。这和编译器有关,我们应该避免这种写法。 -
实现对已经排序的整数二分查找
//使用不对称边界写的数组方式 int* bsearch(int *t, int n, int x) { int lo = 0, hi = n; while(lo < hi) { int mid = (hi + lo) >> 1; if(x < t[mid]) hi = mid; else if(x > t[mid]) lo = mid + 1; else return t + mid; } return NULL; } //使用不对称边界写的指针方式 int* bsearch(int *t, int n, int x) { int *lo = t, *hi = t + n; while(lo < hi) { int *mid = lo + ((hi - lo) >> 1); if(x < *mid) hi = mid; else if(x > *mid) lo = mid + 1; else return mid; } return NULL; } //使用对称边界写的数组方式 int* bsearch(int *t, int n, int x) { int lo = 0, hi = n - 1; while(lo <= hi) { int mid = (hi + lo) >> 1; if(x < t[mid]) hi = mid - 1; else if(x > t[mid]) lo = mid + 1; else return t + mid; } return NULL; } //使用对称边界无法改写指针方式,因为不能把hi初始化为t+n-1,当n=0时,t-1地址无效 //如果还想将程序改写指针形式,就必须对n=0单独处理
第四章——连接
典型的连接器把由编译器或者汇编器生成的若干目标模块,整合到成一个被称为载入模块或者可执行文件的实体,该实体能够被操作系统直接执行。
连接器输入的是一组目标模块和库文件,输出的是载入模块。连接器读入目标模块和库文件,同时生成载入模块。对每个目标模块中的每个外部对象,连接器都要检查载入模块,看是否已经有同名的外部对象。如果没有,连接器就将该外部对象添加到载入模块中;如果有,连接器就开始处理命名冲突。大多数连接器都禁止同一个载入模块中有两个不同外部对象拥有相同的名称。
#include<stdio.h>
main()
{
int i;
char c;
for(i = 0; i < 5; i++)
{
scanf("%d", &c);
printf("%d ", i);
}
printf("\n");
return 0;
}
表面上该程序是从标准输入设备读入5个数,在标准输出设备上写5个数:0 、1、2、3、4
实际上,某编译器输出的结果是:0、0 、1、2、3、4
原因:c被声明为char类型,而不是int类型。当程序要求scanf读取一个整数时,**应该传递给它的是一个指向整数的指针,而程序中得到的却是一个指向字符的指针,**因为整数所占的存储空间要大于字符所占的存储空间,所以字符c附近的内存将会被覆盖。
第五章——库函数
- 返回整数的getchar函数
#include<stdio.h>
main()
{
char c;
while((c = getchar()) != EOF)
putchar(c);
return 0;
}
该程序乍一看似乎是把标准输入复制到标准输出,实则不然
因为程序中的变量c
被声明为char
类型,而不是int
类型,这意味着无法容纳所有可能的字符,特别是可能无法容纳EOF
。所以实际运行的结果可能是:
- 某些合法的输入字符在被“截断后”使得
c
的取值与EOF
相同 c
根本不可能取到EOF
这个值,将会陷入死循环- 还可能运行正常,但是完全是巧合。编译器在比较表达式中并不是比较
c
和EOF
,而是比较getchar
函数的返回值与EOF
- 缓冲输出与内存分配
#include<stdio.h>
int main()
{
int c;
static char buf[BUFSIZ];
setbuf(stdout, buf);//在buf中缓冲
//setbuf(stdout, (char*) 0);//强制不允许对输出进行缓冲
while((c = getchar()) != EOF)
putchar(c);
return 0;
}
setbuf
函数会通知输入/输出库,所有写入到stdout
的输出都应该使用buf
作为输出缓冲区,直到buf
缓冲区被填满或者程序员直接调用fflush
,buf
缓冲区中的内容才实际写入到stdout
中。缓冲区的大小由系统头文件<stdio.h>
的BUFSIZ
定义。数组定义为静态数组,可以保证buf
缓冲区最后一次清空是在整个程序结束之后。
第六章——预处理器
函数调用都会带来重大的系统开销,getchar
和putchar
经常会被实现为宏,以避免在每次执行输入或者输出一个字符时这样简单的操作时,都要调用相应的函数而造成系统效率的下降
- 不能忽视宏定义中的空格
- 宏并不是函数,最好将宏定义中的每个参数都用括号括起来
- 宏并不是语句,宏定义后不能加分号
- 宏并不是类型定义
#define T1 struct foo *
typedef struct foo *T2;
T1 a, b;//struct foo * a, b;
T2 a, b;//(struct foo *) a, b;
第一个语句中的a会被定义为一个指向结构体的指针,而b缺被定义成一个结构而不是指针;第二个语句则不同,它定义了a和b都是指向结构的指针。
第七章——可移植性缺陷
这章主要讲了在不同编译器,不同硬件环境下程序运行结果可能会完全不同。其中包括函数命名,数据长度,默认是有符号数还是无符号数,移位运算,除法截取的不同的例子。
-
标识符名称的限制:谨慎的选择外部标识符的名称是重要的
-
整数的大小
short型整数能容纳的值肯定能被int型整数容纳,int型整数容纳的值也肯定能被long型整数容纳
一个普通int类型整数足够大以容纳任何数组下标
字符长度由硬件特性决定
-
字符是有符号整数还是无符号整数:通常负数范围比正数范围大
-
位移运算符
如果被位移的对象是无符号数,那么空出的位将会被0填充;如果位移的对象是有符号数,那么C语言实现既可以用0填充空位也可以用符号位的副本填充空位
如果被移位的对象长度是n位,那么移位的计数必须大于或者等于0,而严格小于n
-
内存位置0:null指针并不指向任何对象
-
除法运算时发生的截断:余数与被除数的正负号相同
-
随机数大小:会根据当前计算机硬件位数而不同
-
大小写转换
//函数实现 int toupper(int c) { if(c >= 'a' && c <= 'z') return c + 'A' - 'a'; return c; } //宏定义实现 #define _toupper(c) ((c) + 'A' - 'a') #define _tolower(c) ((c) + 'a' - 'A')
-
首先释放,然后重新分配
调用realloc函数时,需要把指向一块已经分配内存的区域指针以及这块内存新的大小作为参数传入,就可以扩大或者缩小这块内存区域为新的大小,这个过程有可能涉及内存的拷贝
-
实现函数
atol
,接受一个指向以null
结尾的字符串的指针作为参数,返回一个对应的long
型整数值long atol(char *s) { long r = 0; int neg = 0; switch(*s) { case '-': neg = 1; //此处没有break case '+': s++; break; } while(*s >= '0' && *s <= '9') { int n = *s++ - '0'; if(neg) n = -n; r = r * 10 + n; } return r; }
-
printf打印格式
printf("%.2d/%2d/%4d\n",7,3,1987);//07/ 3/1987 printf("%+d %+d %+d\n",-5,0,+4);//-5 +0 +4//+表示以数字的符号位作为第一个字符 printf("%-14s","sdfasdf");//-表示显示方式以左端对齐 printf("% d% d\n",-10,90);//-10 90//空格符表示某数是非负数,就在前面插入一个空白符