AndrewKoenig自己在Bell实验室时发表的论文为基础,结合自己的工作经验扩展成这本对C程序员具有珍贵价值的经典著作。写作本书的出发点不是要批判C语言,而是要帮助C程序员绕过编程过程中的陷阱和障碍。
本书所揭示的知识,至少能够帮助你减少C代码和初级C++代码中90%的Bug!!!
下面是博主看完后整理的。(禁止抄袭,违者必究法律责任!)这是第一大篇,共三篇。
@[目录]

1词法分析中的贪心法 2字符与字符串
3理解函数声明 4运算符优先级
5语句结束标志 6函数调用
7悬挂else引发的问题 8数组与指针
9非数组指针 10作为参数的数组声明
11避免“举偶法 12空指针并非空字符串
13边界计算与不对称边界 14整数溢出
15为main函数提供返回值

词法分析中的贪心法
首先,C语言有单字符符号,如 / 、* 、=,和多字符符号,如==、+=。那么当C编译器读入一个字符 ’/‘ 后又有一个 ’‘ ,编译器会以一个规则进行分析:每一个符号应该包含尽可能多的字符,所以 / 同时出现时会被当做一个符号 ’/*‘ ,而不是两个单独的符号。也就是说,编译器从左到右一个一个地读取字符,直到所读取的字符和前一个符号所包含的字符不能构成一个有意义的字符时,前一个字符到此前结束。
举个例子:(按规则分析)
本意是x除以p所指向的值,再把商赋值给y。显然下面这个例子会被当成y=x进行处理。

y=x/*p;

可以修改为:

y=x / *p;//注意空格
//或者
y=x/(*p);

再举一个例子: c语言n–>0的含义是 n-- >0 , 而不是 n- -> 0。
需要注意的是,除了字符串和字符常量,符号中间不能有空白(空格符、制表符、换行符)。
比如: a+++++b在编译时会报错,而(a++)+(++b)和a++ + ++b不会报错。

字符与字符串
在C语言中,字符串常量代表一块包括字符串中所有字符以及一个空字符的内存区域的地址。由此对于其他字符串,也沿用这一惯例。
单引号括起的一个字符代表一个整数,而双引号括起的一个字符代表一个指针。要避免两者混用。
举个例子:

char *p='/';

这个在编译时会出现错误,因为’/‘ 并不是一个字符指针。然而,C编译器对函数的参数,尤其是printf 的函数参数不做类型检查,在程序运行时就会产生难以预料的错误,而且编译器不会给出任何错误。
举个例子:

printf('\n');
printf("\n");

当然,现在编译器一般能检测到函数在调用时混用单引号和双引号的情况。

理解函数声明
任何C变量的声明都·由两部分组成:类型及一组类似的表达式的声明符。
举个例子:

float *g(), (*h)();

表示* g() 与(*h)() 的浮点表达式。因为() 优先级高于 * ,g() 也就是(g()),g是一个函数,该函数的返回值类型为指向浮点数的指针。而h是一个函数指针,所指向函数的返回值为浮点类型。
一旦我们知道如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到:只需把声明中的变量名和末尾的分号去掉,再将剩余部分用一个括号封装起立即可。
举个例子:
float (h)();h 表示一个指向返回值为浮点类型的函数的指针,则 float ()() 表示一个指向返回值为浮点类型的函数的指针的类型转换符。

运算符优先级
任何一个逻辑运算符的优先级低于任何一个关系运算符; 移位运算符的优先级比算数运算符低,但比关系运算符高。

语句结束标志
①下面这两段代码一样:

if (x[i]> s);s=x[i];if(x[i]>s) { }s=x[i];

②C语言允许初始化列表中末尾出现多余的逗号 int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, };
为什么这种特性是有用的?
看下面这段代码:

int main(int argc,char** argv){    enum qs{        Va = 5,        Vb,        Vc,    } s;

像enum枚举中的条目有可能是根据其它资源来的,而程序员会制作代码生成工具来生成它,所以就允许enum中的多余逗号。像这种代码,如果用代码生成器去读资源然后写进去,那么每生成一行都需要在末尾加一个逗号来分隔。C允许最后一行有多余的逗号,那就可以不用单独去处理最后一行了,减轻了代码生成器的制作负担,同时因为不用自行删除最后一个逗号,代码生成器的编写也就更不容易出错。

函数调用
声明一个函数 f() {…} ; 那么 f() 表示函数调用, f 表示计算函数地址,但不调用函数,实际上就是什么也不做。

悬挂else引发的问题
下面这段代码编程者的本意是x等于及x不等于0。

if (x==0)if(y==0) error();else{z=x+y;}

但实际上是下面这样执行的,与实际意图相差甚远。

if (x==0){if (y==0)errror();else{z=x+y;}
}

记住,else始终与同一对括号内最近为匹配的if 结合!!!

数组与指针
①C语言只有一维数组,由数组的元素是任意类型可以抽象出多维数组。任何一个数组的下标的运算实际上都是通过指针进行的。
②任何指针都是指向某种类型的变量。如果一个指针指向的是数组的一个元素,那么我们给这个指针加1,就能够得到指向下一个元素的指针。
③给指针加上一个整数,与给该指针的二进制表示加上同样的整数,两者含义截然不同。比如p指向一个整数,那么p+1指向的是计算机内存的下一个整数。
④请看下面这段代码:

int a[3];
int *p;
p=a;//p=&a是错的

p=&a是错的,因为&a是一个指向数组的指针,而p是指向整型变量的指针。类型不匹配。
所以,a表示数组中下标为0的元素,a=12;表示数组a中下标为0的元素的值设置为12。同样的道理,(a+1)是数组a中下标为1的元素的引用。它被简记为a[1],即(a+i)=a[i]。实际上,a+i 和 i+a 的含义一样,因此a[i]和i[a]含义一样,但绝对不推崇后一种写法!

int s[12][34];
int *p;
int i;
i=s[4][7];
i= * (s[4]+7);
i= * (*(s+4)+7);

后面三个表达式的意思一致。

p=s;//错误❌

因为s是一个二维数组,即数组的数组,使用s的名称会将其转化为一个指向数组的指针;而p是一个整型变量的指针。
声明指向数组的指针的方法:

int s[12][34];
int (*p)[34];
p=s;//指向s的第一个元素

很明显合法。显然,我们可以用指针p遍历数组s。

int s[12][34]=0;//相当于下面的代码for(p = s; p<&s[12];p++){int * q;for(q=*p;q<&(*p)[34];q++)*q=0;}

非数组指针
从一个例子开始:将字符串s和t 连接成单个字符串r。要做到这一点,可以借助库函数strcpy和 strcat 。

char r[100];
strcpy(r,s);
strcat(r,t);

这样确实实现了,但我们不知道字符串s和t的大小,数组r是否能否容纳。库函数还有一个strlen,可以计算字符串的大小。所以我们采用下面这种方法:

char *r,*malloc();
r= malloc(strlen(s)+strlen(t)+1);
if (!r){complain();exit(1);}
strcpy(r,s);
strcat(r,t);
free(r);

首先,给分配的内存使用完后应该立即释放。前面第一段代码r是作为一个局部变量,所以当离开作用域时,r 自动释放,但第二段代码显式的给 r 分配内存,所以要显式的释放 r 内存。 最重要的一点,函数strlen()是返回字符串中所包含的字符数目,而作为结束标志的空字符并未计算进去。所以我们必须为 r 多分配一个字符的空间,这也是为什么要加一的原因。

作为参数的数组声明
如果我们使用数组名作为参数,那么数组名会立刻被转换为指向数组第一个元素的指针。
举个例子:该数组名作为参数传递给函数与将该数组第一个元素的地址作为参数传递给函数的作用等效。下面两个输出是一致的。

char hello[]="hello";printf("%s",hello);printf("%s",&hello[0]);

所以,将数组名作为函数参数就相当于C语言自动将作为参数的数组声明转换为相对应的指针声明。
举几个例子:

int strlen(char s[])
{...}
int strlen(char* s)
{...}
main (int argc, char *argv[])
{...}
mian (int argc, char **argv)
{...}

前一种写法侧重强调argv 是一个·指向某数组起始元素的指针,该数组的元素为字符指针类型。

避免“举偶法”
就是C语言中一个常见的陷阱:混指针与指针所指向的数据。
举个例子:

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

p是指向’x’ 、‘y’、 ‘z’、 '\0’这四个字符组成的数组的起始元素的指针。p和q指向同一地址,赋值指针并不同时复制指针所指向的数据。

空指针并非空字符串
C编译器由0 转换而来的指针不等于任何有效指针。C语言中定义 #define NULL 0
要注意一点,当常数0 被转化为指针使用时,这个指针绝对不能被解除引用。也就是说,当我们把0 赋值给一个指针变量时,不能企图使用该指针所指向的内存中存储的内容,不能访问空指针。

边界计算与不对称边界
C语言中数组的下标为0~n-1,这时为什么呢?我们先看一段代码:

int i,a[10];
for(i=1;i<=10;i++)a[i]=0;

显然,这段代码有误。循环体内将并不存在的a[10]设置为0,也就是内存在数组后的第一个字的内存被设置为0,实际上是将i 的值设置为0,这就陷入了一个死循环。
思考这个问题:100M长的围栏每个十M需要一根支撑用的栏杆,一共需要多少栏杆?用100➗10=10,所以答案是10根。这就是“栏杆错误”。正确答案是11根。
再看一个例子:x>=16&&x<=37,此范围内x的取值有几种可能?答案是22。
存在一种编程技巧,可以减少这种错误的出现——用第一个入界点和第一个出界点来表示一个数值范围。注意,这里下界是入界点,包含取值范围,上界是出界点,不包含在范围内。所以x>=16&&x<=37我们一般这样写:x>=16&&x<38,这样用38-16就是取值可能性的22种。这是一种不对称的形式。所以数组的上界就是数组元素的个数。
举个例子:我们采取第一种写法,而不采用第二种。

int a[10],i;
for(i=0;i<10;i++)a[i]=0;for(i=0;i<=9;i++)a[i]=0;

整数溢出
当两个操作数都是有符号整数时,才有可能发生溢出。当一个运算发生溢出时任何假设都是不安全的。
要检察两个有符号的整数是否溢出的两种方法:
一种是将整数都强制转换为无符号整数:

if ((unsigned)a+(unsigned)b >INT_MAX)complain();
if (a> INF_MAX-b)complain();

为main函数提供返回值
如果为声明返回值类型,系统默认整型。大多数C语言实现都是通过main函数返回值来告知操作系统该函数的执行是失败还是成功。所以main函数最好要有return 语句。返回值0表示执行成功,非0则表示失败。

请继续看剩下的两篇
C语言常见困惑、错误集锦(中) ——《C陷阱与缺陷》3篇
C语言常见困惑、错误集锦(下) ——《C陷阱与缺陷》3篇

C语言常见困惑、错误集锦(上) ——《C陷阱与缺陷》3篇相关推荐

  1. 中国作者论文写作中的常见句法错误(上)

    在科学研究国际化的大背景下,不少科研机构将在高影响因子杂志上发表文章的数量作为研究者晋升的重要标准,一些高等院校也将发表高因子SCI 论文作为研究生毕业的重要条件.由于绝大部分高影响因子杂志都是英语期 ...

  2. C语言常见的错误及解决办法,避坑笔记

    一.指针没有指向一块合法的内存 定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内存.浅显的例子就不举了,这里举几个比较隐蔽的例子. 1.结构体成员指针未初始化 struct stud ...

  3. C语言常见基础错误大全总结

    1.语句分号错误,引号后忘记加逗号,大小写错误 scanf("%c"&a); 2.输入中的取地址符错误 int a; scanf("%d", & ...

  4. 初学C语言常见的错误

    C语言中错误大致分为三类 目录 一.编译型错误 二.链接型错误 三.运行时错误 一.编译型错误 这类问题最简单,往往是语法错误导致的,也比较容易解决. 如:逗号的使用,分号的添加,括号的对应,各类操作 ...

  5. C语言常见的错误类型

    目录 语法错误 逻辑错误 系统错误 语法错误 语法错误: 这是指程序中含有不符合语法规定的语句. 例如: 关键字或符号书写错误(将printf写成print.将数组元素引用写成a(2)等).使用了未定 ...

  6. 零基础初学c语言常见的10个错误

    时光匆匆,新的学年又即将要来临.很多有意愿报我们学院的准学弟学妹们,都开始提前学习c语言这门功课. 如有小伙伴想自学,可以进群731871503进行交流学习,提升编程,共同进步 但是很多学弟学妹们之前 ...

  7. C语言常见的内存错误

    在C语言常见的错误为内存错误,在这里列举一些常见的内存错误. 1.内存泄露 在堆上分配的内存,如果不再使用,就应该把它释放掉,以便后面其他地方可以重用.在C/C++中,内存管理器不会帮你自动回收不再使 ...

  8. C语言/C++常见习题问答集锦[八十三]之数据结构顺序表(operand types are error: no match for “operator==“)

    C语言/C++常见习题问答集锦[八十三]之数据结构顺序表{operand types are error: no match for "operator=="} 程序之美 前言 主 ...

  9. Android NDK开发(三)——常见错误集锦以及LOG使用,androidndk

    Android NDK开发(三)--常见错误集锦以及LOG使用,androidndk 转载请注明出处:http://blog.csdn.net/allen315410/article/details/ ...

最新文章

  1. 得到win7 win8的桌面句柄
  2. 在app中从下向上滑动,以找到不在默认第一页的元素
  3. 设计模式:建造者模式
  4. 软件项目管理的内在定律
  5. 巧用 Protobuf 反射来优化代码,拒做 PB Boy
  6. linux下tomcat脚本,Linux下重启多个 tomcat 服务的脚本(推荐)
  7. 卢卡奇总体性原则_卢卡奇总体性中的现实主义与人性构建
  8. 区块链 Solidity中uint转string 数字转字符串
  9. 【图像配准】基于matlab GUI光流场模型图像配准【含Matlab源码 831期】
  10. Android 实现最新QQ登陆页面
  11. 单月销量破万台,FITURE治好了健身镜的“水土不服”?
  12. Tomcat 在IE中下载rar文件直接以乱码方式打开解决方案
  13. 微信小程序实现身份证识别功能
  14. Python程序设计基础案例
  15. WPF如何实现一款类似360安全卫士界面的程序?(共享源码!)
  16. [附源码]Nodejs计算机毕业设计基于JAVA的校园电车租赁系统Express(程序+LW)
  17. SpringMVC之HandlerMethodReturnValueHandler
  18. 简单有效又有用的关闭antimalware service executable的方法·备份记录
  19. mysql中RowNum的实现
  20. 河道中心线提取、平均宽度计算(Arcgis+CAD)

热门文章

  1. [转载]“董永遇到七仙女”的历史真相
  2. 小米解锁BL锁(普通解锁)
  3. Spark 去掉前几行 去掉表头
  4. 通信模组中的常见术语
  5. 最新威盾IPguard 4.71 18个模块 加密 文档云备份 软件中心 水印与追溯
  6. 4种方法求2个数公因数
  7. 客官请留步!年销千万台的智能香薰加湿器震撼来袭
  8. 尚硅谷李立超老师讲解web前端网课的笔记
  9. php下载效率不高,解决PHP下载大文件失败,并限制下载速度
  10. PowerDesigner概述