《C陷阱与缺陷》学习笔记(一)

前言和导读

  “得心应手的工具在初学时的困难程度往往超过那些easy上手的工具。”比較认同这句话。

我至今认为自己事实上还是个刚入了门的刚開始学习的人。

 

第一章  “词法”陷阱

 

  因为之前学过编译原理,对编译器词法分析(主要是符号识别过程)比較了解,理解起来不困难。

  在讲到"="和"=="、"|"和"||"、"&"和"&&"时,联想起曾经见过一些程序中出现了类似于"#define ||  OR"这种语句。当时以为可能是为了照应习惯其它语言的使用者的阅读偏好,如今看来这样做确实能够避免一些错误。当然使用不使用这种编程风格就是另外一回事了。对于词法分析的运用到的贪心法,之前尽管知道原理和规则,但确实没有意识到这是种贪心法。这样一来,对于easy引起编译器错误的格式,写成还有一种格式更好一些。

y = x/*p // 本须要进行指针取值、除法和赋值,这个颜色已表示编译器并不这么觉得,因此仅仅能写成下一种形式
y = x/(*p)

  至于为整型常量用0补首位以便对齐,反而使得编译器将其误觉得八进制数的情况倒是从来遇到也没考虑到过。

  “单引號' '中的字符上代表一个整数,双引號" " 引起的字符串代表的是一个指向无名数组起始字符的指针。”前半句曾经就知道。后半句有点意思。后半句解释了为什么char *slash = '/' 这个语句有错误。

同一时候。书中指出整型一般为16位或32位,可以容纳多个字符(一般为8位),所以用单引號的'yes'也许可以被一些编译器正确识别。仅仅是巧合而已。有的编译器会将'yes'多余字符忽略,仅仅取第一个整数'y'。有的则依次取值、覆盖再取值,最后结果是仅仅取了最后一个整数相当于's'。

 

11月16日

第二章  “语法”陷阱

 

  模拟调用首地址为0的子例程的语句(*(void(*)())0) ()以及它从(*fp)()生成而来的过程。把常数0转型为“指向返回值为void的函数的指针”的类型就是(void(*)()) 0,用它取代fp就生成了这个语句。

使用typedef会更方便直观。依据补充阅读,在编程中使用typedef目的一般有两个。一个是给变量一个易记且意义明白的新名字,还有一个是简化一些比較复杂的类型声明。

前者早已知道,后者确实是个盲点。因此曾经总把typedef当做还有一种#define。

typedef void (*funcptr)();
(*(funcptr)0)();

利用这个特性,不难理解signal函数的声明。它接受两个參数(一个整型的信号编号和指向用户定义的信号处理函数的指针)。返回值是一个指向调用前的用户信号处理函数的指针。

void (*signal(int,void(*)(int)))(int)
/*以下是简化后的函数声明*/
typedef void (*HDNDLER)(int);
HANDLER signal(int, HANDLER)

  关于运算符优先级。曾经的处理方式是无脑加括号,全然不关注;可是括号太多了确实会造成阅读困难。

依据原书所看到的做出归纳。

  多余的和缺失的分号会造成的错误一般不会出现。可是所给的样例比較醒目地提醒了结构定义后假设不加分号的结果——可能导致其后定义的函数返回类型为这样的结构:

struct logrec{
......
}
main() //当然一般使用void main()

  有时候,switch...case...结构中不一定每个分支语句都须要break,书中字符处理的样例就不再反复。

  没有參数的函数调用也应该有一个空的參数列表。

  else总与近期的if配对。

 

  其它的一些感想:对于词法分析。空格、换行确实是可行的分隔方法;然而在语法分析时却不那么好用了。多余的空格和换行符往往会被删掉,因此必须明确语句结构的配对原则和分号" ; "及逗号" , "的正确使用,还有大括号" { } "的配对。当然这个结论应该是初学伊始就牢牢树立的观点了。

有的语言特点和编程风格(特别是通过宏定义改变语法结构的外观)不少是从包含C语言的前身以及其它语言的编程风格继承而来,但后者并非必要的。

 

11月16日~17日

第三章  “语义”陷阱

 

  数组和指针:数组名实际上是一个指向数组首元素的指针。仅仅有在作为sizeof()的參数时例外。

  尽管C语言仅仅有一维数组。但用一维数组模拟多维数组是能够的。

  a[i]是*(a+i)的简记,这也就能够解释为什么数组首元下标从0開始。也正因此,a[i]和i[a]具有同样的含义。汇编中后者并不少见。但作者不推荐这种写法。在多维数组中,下标表示法比*(*(calender+4)+7)这种表示方法简单多了。

  字符串拷贝中暗含的陷阱:malloc分配空间可能失败;分配后须要及时释放。分配内存大小的限定。三者结合起来的样例:

三个陷阱

  C中无法将一个数组作为函数參数直接传递。假设使用数组名作为參数。那么数组名会立马被转换为指向该数组第1个元素的指针。char hello[] = "hello"声明了一个字符数组后。printf("%s ",hello)和printf("%s ",&hello[0])是等效的(相同是在终端显示hello)。但这样的情况限于其作为函数參数的情况,书中指出,假设这样的自己主动转换在其它情形下成立是错误的,"extern char *hello"与"extern char hello[]"有着天差地别,这是第四章内容,尚未涉及。由此延伸而来。假设一个指针參数并不实际代表一个数组。即使从技术上而言是正确的,採用数组形式的记法常常会起到误导作用。

反之。假设一个指针參数代表一个数组。以main函数第二个參数为例来说明:

main(int argc, char* argv[]) {  } 
//强调argv是一个指向某数组的起始元素的指针。该数组的元素为字符指针类型

main(int argc, char** argv) {  }
//两种写法全然等价,能够任选一种最能清楚反应自己意图的写法

  避免“举隅法”(不必深究这个语言学名词),包括的“陷阱”即为混淆指针和指针所指的数据。比方char *p,*q;p="ABC";q=p;。
  除了0。C语言将一个整数转换为一个指针。最后得到的结果取决于编译器实现。而0。编译器保证其转换而来的指针不等于不论什么有效的指针。

#define NULL 0 是出于代码文档化的考虑。因此是不能使用赋值为0的指针变量所指向的内存中存储的内容的。这是没有定义的。相关的语句在不同计算机上会有不同效果。第七章会具体讨论这个问题。

  在“边界计算和不正确称计算”这个“陷阱”处,作者以int i,a[10]举例。说明了为什么for(i=0; i<10; i++)   a[i] = 0 ;比for(i=0; i<=9; i++)   a[i] = 0 ;要好:入界点和出界点恰好为0和10,而且对于下标为从0開始的C语言,出界点恰是数组元素个数。

对于这个问题的还有一种考虑方法:把上界视作某序列第一个被占用的元素。把下界视作第一个被释放的元素。这样的考虑方式处理不同类型的缓冲区时非常实用,所举样例是一个指向缓冲区的指针,让它总是指向第一个未占用的字符。这样对其赋值就有 *bufptr++ = c;的形式。对于样例还有很多其它细节能够揣摩。

边界计算与不正确称边界的偏好

  在之后的还有一个样例中,作者觉得,技巧性非常强的代码,“假设没有非常好的理由,我们不应该尝试去做。

但假设是‘师出有名’,那么理解这种代码应该怎样写就非常重要了。

”对于那个详细的样例。“仅仅要我们记住前面的两个原则,特例外推法和细致计算边界,我们应该全然有信心做对。”

还有一个样例

  作者提到,之前讨论了运算符优先级的问题。“求值顺序则全然是还有一码事”。前者是保证a + b * c应该解释成a + (b * c)而不是(a+b) * c这样一类的规则,求值顺序是保证if (cout != 0 && sum/count < smallaverage) {...}即使count为0也不会产生用0作除数的错误的规则。

这里有一篇别人的日志能够參考。

要点在于,C语言中仅仅有四个运算符(&&、||、? :和,)存在规定的求值顺序。特别指出用于分隔函数參数的逗号并不是逗号运算符。其它运算符对其操作数求值的顺序是没有定义的,赋值运算符并不能保证不论什么求值顺序。

复制代码
i = 0;
while (i < n)
    y[i] = x[i++];
//这里有个如果:y[i]的地址在i的自增操作运行前被求值,但实际并没有不论什么保证。有的C语言实现可能如此。有的则相反。
// y[i++] = x[i];相同有这样的问题

/*改进*/
i = 0;
while (i < n) {
    y[i] = x[i++];
    i++;
}
/*简写*/
for (i = 0; i < n; i++)
    y[i] = x[i];
复制代码

  作者特别提到。尽管用&、|、~和&&、||、!相应运算相互替代时,程序执行结果可能是正确的,并详解了一下,但这侥幸成分非常大并且绝少有C编译器可以检測出,因此这样的错误还是应该要避免。

  无符号数运算不会溢出:全部无符号运算都是以2的n次方为模,在这个意义上确实没有“溢出”这一说。相关解释能够參考这里

对有符号数运算溢出的检測方法:

复制代码
if ((unsigned)a + (unsigned)b > INTMAX) 
//INT_MAX代表可能的最大数值。ANSI C在<limits.h>定义
//其它C语言实现中或许要自定义
    complain();

/*还有一种可行方法*/
if (a > INT_MAX - b)
    complain();
复制代码

  为main提供返回值的原因:大多数C语言实现都通过main返回值来告诉操作系统该函数运行是成功还是失败。一般0代表成功,非0为失败。不给出返回值的结果是隐含地返回了某个“垃圾”整数(未显式声明返回类型则默觉得整型)。

原文地址:https://www.cnblogs.com/gcczhongduan/p/5284373.html