文章参考于文献:《C陷阱与缺陷》[美]Andrew Koening
🔥 创作专栏:《史上最强算法分析》 | 《无味生》 |《史上最强C语言讲解》 | 《史上最强C练习解析》
🏆我的格言:一切只是时间问题。
目录
词法陷阱
一、= 不同于 ==
在 if 判断时容易出错。
=:赋值运算,a=3;
表示的是将3赋值给a变量。
==:比较运算,a==3;
表示判断a是否等于3,若等于则返回1,否则返回0。
二、& 和 | 不同于 && 和 ||
这是两个 逻辑操作符与位操作符的区别。
三、词法分析中的“ 贪心法 ”
a---b;等于a-- -b; //先a-b,再a--
不等于a- --b; //--b先做自减运算,再a-b
四、整型常量
如果一个整型常量的第一个数字是0,则该常量会被当做是八进制数,因此10和010代表是分别是十进制的10和十进制的8。
五、字符与字符串
单引号引起来的字符代表的是该字符的ASCII码值;
双引号引起来的字符串代表的是一个指向无名数组的起始字符的指针,该数组被双引号之间的字符以及一个额外’\0’(字符串标志)初始化。
在双引号引起来的字符串中,注释符号/*属于字符串的一部分;在注释中出现的双引号“”又属于注释的一部分。
因此,语句:printf("The world") 和以下语句是等价的:
char str[]= {'T','h','e',' ','w', 'o','r','l','d','\n'};
printf(str);
语法陷阱
一、理解函数声明
指针例题
二、运算符的优先级问题
详见 优先级
三、注意作为语句结束标志的分号
//代码1
if(x[i] > b);
b = x[i];
//代码2
if(x[i] > b){}
b = x[i];
//代码3
if(x[i] > b)
b = x[i];
一个分号就代表一个语句的结束。代码1与代码2是等价,if和赋值语句是两个独立的语句;而第三句中赋值语句在if中。
四、swith语句
五、函数调用
C语言要求:在函数调用时即使函数不带参数也应该包括参数列表。因此,如果f是一个函数,
f();//是一个函数调用语句。
而
f;//却是一个什么也不做的语句
更精确地说,这个语句计算函数f的地址,却并不调用该函数。
六、“悬挂”else引发的问题
if (x == 0)
if (y == 0) error();
else {
Z = X + Y;
f(&z);
}
两种解读:
解读一:
if (x == 0)
{
if (y == 0) error();
else
{
Z = X + Y;
f(&z);
}
}
解读二:
if (x == 0)
{
if (y == 0) error();
}
else
{
Z = X + Y;
f(&z);
}
语义陷阱
一、指针与数组
深入理解指针系列文章
二、非数组的指针
1、假设我们要将两个字符串s和t连接组成一个字符串r,我们可以借助库函数strcpy()和strcat():
char *r;
strcpy(r,s);
strcat(r,t);
这样子其实无法实现,因为r所指向的地址还应该有内存空间可供容纳字符串,这个空间应该是以某种方式被分配了的。
2、所以我们此次给r分配一定的存储空间:
char r[100];
strcpy(r,s);
strcat(r,t);
可以看到,我们分配的大小为100,但是我们无法确保r足够大可以容纳s和t连接之后的字符串。
3、因此我们可以利用malloc()函数来分配内存:
char *r, *malloc();
r = malloc(strlen(s) + strlen(t));
strcpy(r,s);
strcat(r,t);
这样我们就手动给r开发了一块内存,大小为s和t字符数的和。
4、不过这样还是不够好,malloc()函数有可能无法提供请求的内存,这样的情况下malloc函数会通过返回一个空指针来作为“内存分配失败”的事件信号。下面是较为完善的代码:
char *r, *malloc();
r = malloc(strlen(s) + strlen(t) + 1); //需要多分配一个内存空间,以装一个空字符作为字符串结束标志
if(!r) // 判空语句
{
complain();
exit(1);
}
strcpy(r,s);
strcat(r,t);
free(r); //释放暂时申请的内存空间r
三、作为参数的数组声明
在函数中传递参数时,C语言会自动地将作为参数的数组相地转换为指针声明,也就是说:
int strlen(char s[]){ ; }
int strlen(char *s){ ; }
//是等价的
但是这两种是不等价的:
extern char *hello;
extern char hello[];
一个指针参数代表一个参数:
main(int argc, char *argv[]){ ; }
main(int argc, char **argv){ ; }
在main函数中的第二个参数就是一个指针参数代表一个数组的情况。
四、避免“ 举隅法 ”
常见错误解释:避免以整体代表部分,或者以部分代表整体。
常见错误:混淆指针与指针所指向的数据。
char *p,*q;
p ="xyz";
上面的赋值语句使得p的值就是字符串"xyz",然而实际情况并不是这样,实际上,p的值是一个指向由'x'、'y'、’z"和\0 4个字符组成的数组的起始元素的指针。
因此,如果我们执行下面的语句:q=p;
p和q现在是两个指向内存中同一地址的指针,但这个赋值语句并没有同时复制内存中的字符。
复制指针并不同时复制指针所指向的数据,因此,当我们执行完下面的语句之后:q[1]='y';q所指向的内存现在存储的是字符串'xyz’。因为p和q所指向的是同一块内存,所以p指向的内存中存储的当然也是字符串’xyz'。
五、空指针并非空字符串
在C语言中将一个整数转换为一个指针,最后得到的结果都取决于具体的C编译器实现。这个特殊情况就是常数0,编译器保证由0转换而来的指针不等于任何有效的指针。
出于代码文档化的考虑,常数0这个值经常用一个符号来代替:#define NULL 0
当然无论是直接用常数0,还是用符号ULL,效果都是相同的。
需要记住的重要一点是,当常数0被转换为指针使用时,这个指针绝对不能被解除引用(dereference)。换句话说,当我们将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容。
合法格式:
if(p == (char *) 0)
非法格式:
if(strcmp(p,(char*)0) == 0)
原因在于库函数 strcmp 的实现中会包括查看它的指针参数所指向内存中的内容的操作。
六、边界计算与不对称计算
在C语言中,一个拥有10个元素的数组,它的下标范围是从0到9的。那么0就是数组下标的第一个“入界点”(指的是处于数组下标范围以内的点,包括边界点),而10就是数组下标中的第一个“出界点”(指的是数组下标范围以内的点,不包括边界点)。
正因为如此,我们可以这样写:
int a[10], i;
for (i=0; i<10; i++)
a[i] = 0;
而不这样写:
int a[10], i;
for(i=0; i<=9; i++)
a[i] = 0;
七、求值顺序
求值顺序与运算符优先级是不同的两个概念,例如:
a < b && c <d;
该语句中,如果a大于或等于b,则无需再进行c<d的判断了,因为表达式肯定为假。
八、逻辑运算符&&、| |和 !
在有些时候,用按位运算符&、|和~替换掉逻辑运算符&&、||和!,程序看上去还能正常工作,但是实际上这只是巧合所致。
九、整数溢出
C语言中存在两类整数算术运算,有符号运算与无符号运算。
1、两个无符号算术运算中,没有所谓的“溢出”一说:所有的无符号运算都是以2的n次方为模,这里n是结果中的位数。
2、一个操作数是有符号整数,另一个是无符号整数,那么有符号整数会被转换为无符号整数,“溢出”也不可能发生。
3、当两个操作数都是有符号整数时,“溢出”就有可能发生,而且“溢出”的结果是术定义的。当一个运算的结果发生“溢出”时,作出任何假设都是不安全的。
正确的方式是将a和b都强制转换为无符号整数:
if ((unsigned)a + (unsigned)b > INT_MAX)
complain();
此处的 INT_MAX是一个已定义常量,代表可能的最大整数值。ANSIC标准在<limits.h>中定义了INTMAX:如果是在其他C语言实现上,读者也许需要自己重新定义。
//不需要用到无符号算术运算的另一种可行方法是:
if(a >INT_MAX - b)
complain();
十、为函数main提供返回值
函数 main 与其他任何函数一样,如果并未显式声明返回类型,那么函数返回类型就默认为是整型。但是这个程序中并没有给出任何返回值。
通常说来,这不会造成什么危害。一个返回值为整型的函数如果返回失败,实际上是隐含地返回了某个“垃圾”整数。只要该数值不被用到,就无关紧要。
严格说来,我们前面的最简单的C程序应该像下面这样编写代码:
int main()
{
//语句
return 0;
}