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

前言和导读

  “得心应手的工具在初学时的困难程度往往超过那些容易上手的工具。”比较认同这句话。我至今觉得自己其实还是个刚入了门的初学者。

第一章  “词法”陷阱

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

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

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\n",hello)和printf("%s\n",&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为失败。不给出返回值的结果是隐含地返回了某个“垃圾”整数(未显式声明返回类型则默认为整型)。

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

时间: 2024-10-08 04:55:51

《C陷阱与缺陷》学习笔记(一)的相关文章

原型模式和Effective C++学习笔记

原型模式(Prototype):用原型实例制定创建对象的种类,并且听过拷贝这些原型创建新的对象. 浅复制:如果字段是值类型的,则对该字段执行逐位复制,如果字段是引用类型,则复制引用但不复制引用的对象:因此,原始对象及其副本引用同一对象. 深复制:把引用变量的对象指向复制过的新对象,而不是原有的被引用的对象. Effective C++: 1:在资源管理类中提供对原始资源的访问. (1),APIs 往往要求访问原始资源(raw resources),所以每一个 RAII class 应该提供一个"

模板方法模式和Effective C++学习笔记

模板方法模式: 定义:定义一个操作中的算法的骨架,而将一些步骤延伸到子类中.模板方法使得子类可以不改变算法的结构即可重定义该算法的某些特定步骤. (1),用了继承,并且肯定这个继承有意义的情况下,就应该要成为子类的模板,所以重复的代码都应该提升到父类中,而不是让每个子类去重复. (2),当我们要完成某一个细节层次一致的过程或者一系列步骤,但其个别步骤在更详细的层次上的实现可能不同时,我们通常考虑模板方法模式来处理. (3),模板方法模式通过把不变的行为搬移到超类,去除子类中的重复代码来体现它的优

Effective C++学习笔记(Part Seven:Item 41-48)

 最近终于把effectvie C++仔细的阅读了一边,很惊叹C++的威力与魅力.最近会把最近的读书心得与读书笔记记于此,必备查找使用,如果总结有什么不当之处,欢迎批评指正: 现在只列出框架,最近会尽快填充完整: 第7部分:模板与泛型编程 所谓泛型编程就是以独立于任何特定类型的方式编写代码.使用泛型程序时,我们需要提供具体程序实例所操作的类型或值.模板是泛型编程思想的一种实现,也是C++研发者思想精髓所在. 条款41:了解隐式接口和编译器多态 条款42:了解typename的双重含义 条款4

Effective C++学习笔记(Part Six:Item 32-40)

 最近终于把effectvie C++仔细的阅读了一边,很惊叹C++的威力与魅力.最近会把最近的读书心得与读书笔记记于此,必备查找使用,如果总结有什么不当之处,欢迎批评指正: 现在只列出框架,最近会尽快填充完整: 第6部分:继承与面向对象设计 面向对象语言有三大特性:继承,封装和多态.遥想当年,每次校招笔试的时候都会有的,但能够甚至这六个字内涵的人,不再多数.本部分概括的说,深入的讲解三大特性之一----继承性.本部分花的时间不少,也感觉挺有意思的.特别是针对某个条款编写个具体程序实现后,明

Effective Java 学习笔记之第七条——避免使用终结(finalizer)方法

避免使用终结方法(finalizer) 终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的. 不要把finalizer当成C++中析构函数的对应物.java中,当对象不可达时(即没有引用指向这个对象时),会由垃圾回收器来回收与该对象相关联的内存资源:而其他的内存资源,则一般由try-finally代码块来完成类似的工作. 一.finalizer的缺点: 1. 终结方法的缺点在于不能保证会被及时地执行. 及时执行finalizer方法是JVM垃圾回收方法的一个主要功

Effective C++学习笔记(Part Two:Item 5-12)

?? 近期最终把effectvie C++细致的阅读了一边,非常惊叹C++的威力与魅力.近期会把近期的读书心得与读书笔记记于此.必备查找使用,假设总结有什么不当之处,欢迎批评指正: 如今仅仅列出框架.近期会尽快填充完整: 第2部分:构造/析构/赋值运算 个人觉得这是C++的比較难理解的地方,也是C++的深邃所在,刚開始阅读时,可能在某些地方不理解,最好的方式是边读边编程实现,用心去思考其精髓所在! 条款5:了解C++默默编写并调用那些函数 条款6:若不想使用编译器自己主动生成的函数,就该明白拒绝

Effective C++学习笔记之对RAII思想的思考

 在阅读  Effective C++时,对RAII开始不理解,现总结如下: RAII是Resource acquisition is initialization的缩写形式,即"资源获取就是初始化",是C++等编程语言常用的管理资源.避免内存泄露的方法.它保证在任何情况下,使用对象时先构造对象,最后析构对象. 根据RAII 对资源的所有权可分为常性类型和变性类型,代表者分别是boost::shared_ptr和std::auto_ptr:从所管资源的初始化位置上可分为外部初始化类

Effective C++学习笔记(Part Three:Item 13-17)

 最近终于把effectvie C++仔细的阅读了一边,很惊叹C++的威力与魅力.最近会把最近的读书心得与读书笔记记于此,必备查找使用,如果总结有什么不当之处,欢迎批评指正: 现在只列出框架,最近会尽快填充完整: 第3部分:资源管理(Resource Management) 个人认为这是C++的比较关键的地方,利用好,可以使项目节约资源,合理利用内存资源,防止内存泄露.对于"资源"这两个字,可能刚接触,比较抽象,但你结合一下实际,也有很具体了! 条款13:以对象管理资源 条款14:

Effective C++学习笔记(Part Four:Item 18-25)

 最近终于把effectvie C++仔细的阅读了一边,很惊叹C++的威力与魅力.最近会把最近的读书心得与读书笔记记于此,必备查找使用,如果总结有什么不 当之处,欢迎批评指正: 现在只列出框架,最近会尽快填充完整: 第4部分:设计与申明(Designs and Declarations) 本部分主要讲述了类的设计原则以及设计规则,以及成员命令.成员函数设计时,应该注意的一些地方:个人感觉这是设计好一个类必不可少的. 条款18:让接口容易本正确使用,不易被误用 条款19:设计class犹如设计

Effective C++学习笔记(Part Five:Item 26-31)

 最近终于把effectvie C++仔细的阅读了一边,很惊叹C++的威力与魅力.最近会把最近的读书心得与读书笔记记于此,必备查找使用,如果总结有什么不 当之处,欢迎批评指正: 现在只列出框架,最近会尽快填充完整: 第5部分:实现(Implementations) 本部分主要讲述了类实现过程中的一些原则,为设计出高效率的实现方式,这是你的必经之路:在这一部分读的比较仔细,因为本人在类型转换这方面吃了不少苦头. 条款26:尽可能延后变量定义式的出现时间 条款27:尽量少做转型动作 条款28:避