《C专家编程》阅读记录

  • 规避误用赋值
    在比较式中先写常数,如: if(3 ==i),这样,如果不小心误用了赋值符号,就会报错

第一章 C:穿越时空的迷雾

  • 表达式中的数组名可以看作是指针
    把数组当作指针,简化了很多东西。我们不再需要一种复杂的机制区分它们,把它们传递到一个函数时不必忍受必须复制所有数组内容的低效率。不过,数组和指针并不是在任何情况下都是等效的,更详细的讨论参见第4章。

  • register 关键字
    这个关键字能给编译器设计者提供线索,就是程序中的哪些变量属于热门(经常被使用),这样就可以把它们存放到寄存器中。这个设计可以说是一一个失误,如果让编译器在使用各个变量时自动处理寄存器的分配工作,显然比一 -经声明就把这类变量在生命期内始终保留在寄存器里要好。使用register关键字,简化了编译器,却把包袱丢给了程序员。

  • C预处理器实现的3个主要功能:

    • 字符串替换
      形式类似“把所有的foo替换为baz",通常用于为常量提供一个符号名。
    • 头文件包含
      (这是在BCPL中首创的)一般性的声明可以被分离到头文件中,并且可以被许多源文件使用。虽然约定采用“.h”作为头文件的扩展名,但在头文件和包含实现代码的对象库之间在命名上却没有相应的约定,这多少令人不快。
    • 通用代码模板的扩展
      与函数不同,宏(marco)在连续几个调用中所接收的参数的类型可以不同(宏的实际参数只是按照原样输出)。这个特性的加入比前两个稍晚,而且多少显得有些笨拙。在宏的扩展中,空格会对扩展的结果造成很大的影响。
  • 容易混淆的const

    • 关键字const并不能把变量变成常量!在一个符号前加上const 限定符只是表示这个符号不能被赋值。也就是它的值对于这个符号来说是只读的,但它并不能防止通过程序的内部(甚至是外部)的方法来修改这个值。const 最有用之处就是用它来限定函数的形参,这样该函数将不会修改实参指针所指的数据,但其他的函教却可能会修改它。这也许就是C和C++中const最一般的用法。
    • const和*的组合通常只用于在数组形式的参数中模拟传值调用。它声称“我给你一个指向它的指针,但你不能修改它。.这个约定类似于极为常见的void *的用法,尽管在理论上它可以用于任何情形,但通常被限制于把指针从一种类型转换为另一种类型。
    • 类似地,还可以取一个const变量的地址
  • 对无符号类型的建议

    • 尽量不要在你的代码中使用无符号类型,以免增加不必要的复杂性。尤其是,不要仅仅因为无符号数不存在负值(如年龄、国债)而用它来表示数量。
    • 尽量使用像int 那样的有符号类型,这样在涉及升级混合类型的复杂细节时,不必担心边界情况(如一1被翻译为非常大的正数)。
    • 只有在使用位段和二进制掩码时,才可以用无符号数。应该在表达式中使用强制类型转
      换,使操作数均为有符号数或者无符号数,这样就不必由编译器来选择结果的类型。

第二章 这不是Bug,而是语言特性

  • 一个‘L’ 的NUL和两个L’的NULL
    牢记下面的话,它有助于回忆指针和ASCII码零的正确术语:

    • 一个‘L的NUL用于结束一个ACSII字符串,
    • 两个‘L’的NULL用于表示什么也不指向( 空指针)。
    • 当然,如果出现了三个’L’ 的NULL,那就要检查一下有没有拼写错误了。ACSII 字符中零的位模式被称为‘NUL’。 表示哪里也不指向的特殊的指针值则是NULL’ 。 这两个术语不可互换。
  • switch语句

    • 也许switch语句最大的缺点是它不会在每个case标签后面的语句执行完毕后自动中止。一旦执行某个case语句,程序将会依次执行后面所有的case,除非遇到break语句。这称之为 “ fall through ” ,它的意思是:如果case语句后面不加beak, 就依次执行下去,以满足某些特殊情况的要求。但实际上.这是一个非常不好的特性,因为几乎所有的case都需要以break结尾。大部分lint程序在发现 “ fall through " 情况时甚至会发出警告信息。
    • break语句事实上跳出的是最近的那层循环语句或switch语句,
  • sizeof语句
    当sizeof 的操作数是个类型名时,两边必须加上括号(这常常使人误以为它是个函数),但操作数如果是变量则不必加括号。

  • 空格- -最后的领域

    • 许多人会告诉你空格在C语言中没有什么意义,只要你喜欢,随便多输入几个或者少输入几个都没有关系。但事实并非如此!这里有几个例子,空格从根本上改变了程序的意思或程序的有效性。
    • “\”字符可用于对一些字符进行“转义”,包括newline (这里指回车键)。被转义的newline在逻辑上把下一行当作当前行的延续,它可用于连接长字符串。如果在“\”和回车键之间不小心留上一两个空格就会出现问题,“\ newline ”和“\newline”就不一样。这个错误很难被发现,因为你是在寻找某种无形的东西(在应该是newline 的地方出现了一个空格,注意newline并不是一个有形的字符,所以“\”后面有没有空格在实际代码中根本看不出来)。newline在典型情况下用于转义连续多行的宏定义。

第三章 分析C语言的声明

  • 关于结构
    • 跟结构有关的参数传递问题。有些C语言书籍声称“在调用函数时,参数按照从右到左的次序压到椎栈里。”这种说法过于简单了。参数在传递时首先尽可能地存放到寄存器中(追求速度)。注意,int 型变量i跟只包含一个int型成员的结构变量s在参数传递时的方式可能完全不同。一个int型参数一般会被传递到寄存器中,而结构参数则很可能被传递到堆栈中。
    • 第二点需要注意的是,在结构中放置数组,如:
/* 数组位于结构内部 */
struct s_ tag { int a[100] ; } ;

现在,你可以把数组当作第一等级的类型,用赋值语句拷贝整个数组,以传值调用的方式把它传递到函数,或者把它作为函数的返回类型。

  • 关于联合
    联合一般被用来节省空间,因为有些数据项是不可能同时出现的,如果同时存储它们,显然颇为浪费。

  • 关于枚举
    缺省情况下,整型值从零开始。如果对列表中的某个标识符进行了赋值,那么紧接其后的那个标识符的值就比所赋的值大1,然后类推。枚举具有一个优点: #define 定义的名字一般在编译时被丢弃,而枚举名字则通常一直在调试器中可见,可以在调试代码时使用它们。

  • typedef int x[10]和#define x int[10]的区别

    • 在typedef和宏文本替换之间存在一个关键性的区别。正确思考这个问题的方法就是把typedef看成是一种彻底的“封装”类型——在声明它之后不能再往里面增加别的东西。它和宏的区别体现在两个方面。
    • 首先,可以用其他类型说明符对宏类型名进行扩展,但对typedef所定义的类型名却不能这样做。
#define peach int 
unsigned peach i;/*没问题*/

typedef int banana;
unsigned banana i;/* 错误,非法 */
  • 其次,在连续几个变量的声明中,用typedef定义的类型能够保证声明中所有的变量均为同一种类型,而用#define定义的类型则无法保证。如下所示:
#define int_ptr int *
int_ptr chalk, cheese;

经过宏扩展,第二行变为:

int * chalk, cheese;

这使得chalk和cheese成为不同的类型,就好象是辣椒酱与细香葱的区别: chalk 是一个指向int的指针,而cheese则是一个int。 相反,下面的代码中:

typedef char * char_ptr;
char_ptr Bentley, Rolls_Royce;

Bentley和Rolls_Royce 的类型依然相同。虽然前面的类型名变了,但它们的类型相同,都是指向char的指针。

第四章 令人震惊的事实:数组和指针并不相同

  • 区分定义和声明
    只要记住下面的内容即可分清定义和声明:
    • 声明相当于普通的声明:它所说明的并非自身,而是描述其他地方的创建的对象。
    • 定义相当于特殊的声明:它为对象分配内存
  • 使声明与定义相匹配
    数组在声明时就该用 [ ] ,不可用指针代替
  • 数组和指针的其他区别
    • 指针定义的字符串仅可读,(指针变量存储在栈区,字符串储存在常量区(.rodata))
    • 数组定义的字符串可读可写,(数组变量存储在栈区,字符串储存在已初始化数据区)

参考https://blog.csdn.net/ywcpig/article/details/52303745

第六章 运动的诗章:运行时数据结构

  • 内存管理
    • 数据区、堆栈区、文本区(代码区)

参考https://blog.csdn.net/ywcpig/article/details/52303745

  • setjmp和longjmp
    • goto语句不能跳出C语言当前的函数(这也是“longjmp"取名的由来,它可以跳得很远,甚至可以跳到其他文件的函数中)。
    • 用longjmp只能跳回到曾经到过的地方。在执行setjmp的地方仍留有一个过程活动记录。从这个角度讲,longjmp更像是“从何处来(come from)”而不是“往哪里去(go to)”。longjmp接受一个额外的整型参数并返回它的值,这可以知道是由longjmp转移到这里的还是从上一条语句执行后自然而然来到这里的。
    • 需要注意的地方是:保证局部变量在longjmp过程中一直保持它的值的惟一可靠方法是把它声明为volatile (这适用于那些值在setjmp执行和longjmp返回之间会改变的变量)。setjmp/longjmp最大的用途是错误恢复。只要还没有从函数中返回,一旦发现一个不可恢复的错误,可以把控制转移到主输入循环,并从那里重新开始。有些人使用setjmp/ongimp从一串无数的函数调用中立即返回。还有一些人用它们防范潜在的危险代码。在使用setjmp和longjmp的任何源文件中,必须包含头文件<setjmp.h>.

第七章 对内存的思考

  • 内存泄漏
    • 有些程序并不需要管理它们的动态内存的使用。当需要内存时,它们简单地通过分配来获得,从来不用担心如何释放它。这类程序包括编译器和其他一些运行一段固定的(或有限的)时间然后终止的程序。当这种类型的程序终止时,所有内存会被自动回收。细心查验每块内存是否需要回收纯属浪费时间,因为它们不会再被使用。
    • 其他程序的生存时间要长一点。有些工具如日历管理器、邮件工具以及操作系统本身经常需要数日乃至数周连续运行,并需要管理动态内存的分配和回收。由于C语言通常并不使用垃圾收集器(自动确认并回收不再使用的内存块),这些C程序在使用malloc()和 free()时不得不非常慎重。堆经常会出现两种类型的问题:
      • 释放或改写仍在使用的内存(称为“内存损坏”)。
      • 未释放不再使用的内存(称为“内存泄漏”)。这是最难被调试发现的问题之一。如果每次已分配的内存块不再使用而程序员并不释放它们,进程就会一边分配越来越多的内存,一边却并不释放不再使用的那部分内存。

第八章 为什么程序员无法分清万圣节和圣诞节

  • 在等待时类型发生了变化
    C语言中的类型转换比一般人想象中的要广泛得多。在涉及类型小于int 或double的表达式中,都有可能出现类型转换。以下面的代码为例:
printf(" %d ", sizeof 'A');

这行代码打印出存储一个字符字面值类型的长度。你敢确定它的结果就是字符的长度,也就是1吗?那就运行一下代码试试。你会发现事实上的结果是4(或者是你机器上int 的长度)。字符常量的类型是int,根据提升规则,它由char转换为int。
在这里插入图片描述
整型提升就是char、short int和位段类型(无论signed或unsigned)以及枚举类型将被提升为int,前提是 int能够完整地容纳原先的数据,否则将被转换为unsigned int。ANSI C表示如果编译器能够保证运算结果一致,也可以省略类型提升——这通常出现在表达式中存在常量操作数的时候。
表8-1提供了一个常见类型提升的列表。它们可以出现在任何表达式中,并不局限于涉及操作符和混合类型操作数的表达式。
在这里插入图片描述
警惕!真正值得注意之处——参数也会被提升!
另一个会发生隐式类型转换的地方就是参数传递。在K&RC中,由于函数的参数也是表达式,所以也会发生类型提升。在ANSIC 中,如果使用了适当的函数原型,类型提升便不会发生,否则也会发生。在被调用函数的内部,提升后的参数被裁减为原先声明的大小

第九章 再论数组

  • 什么时候数组与指针相同
    • 第4章着重强调了数组和指针并不一致的绝大多数情形。本章的开始部分就是讲述可以把它们看作是相同的情形。在实际应用中,数组和指针可以互换的情形要比两者不可互换的情形更为常见。让我们分别考虑“声明”和“使用”(使用它们传统的直接含义)这两种情况。
      声明本身还可以进一步分成3种情况:
      • 外部数组(external array)的声明。
      • 数组的定义(记住,定义是声明的一种特殊情况,它分配内存空间,并可能提供一个初始值)。
      • 函数参数的声明。
    • 所有作为函数参数的数组名总是可以通过编译器转换为指针。在其他所有情况下(最有趣的情况就是“在一个文件中定义为数组,在另一个文件中声明为指针”,第4章已有所描述),数组的声明就是数组,指针的声明就是指针,两者不能混淆。但在使用数组(在语句或表达式中引用)时,数组总是可以写成指针的形式,两者可以互换。
    • 然而,数组和指针在编译器处理时是不同的,在运行时的表示形式也是不一样的,并可能产生不同的代码。对编译器而言,一个数组就是一个地址,一个指针就是一个地址的地址。
  • C语言标准对此作了如下说明
    • 规则1.表达式中的数组名(与声明不同)被编译器当作一个指向该数组第一个元素的指针(具体释义见ANSI C标准第6.2.2.1节)。
    • 规则2.下标总是与指针的偏移量相同(具体释义见ANSI C标准第6.3.2.1节)。
    • 规则3.在函数参数的声明中,数组名被编译器当作指向该数组第一个元素的指针(具体释义见ANSI C标准第6.7.1节)。
  • 为什么C语言把数组形参当作指针
    把作为形参的数组和指针等同起来是出于效率原因的考虑。在C语言中,所有非数组形式的数据实参均以传值形式(对实参作一份铂贝并传递给调用的函数,函数不能修改作为实参的实际变量的值,而只能修改传递给它的那份拷贝)调用。然而,如果要拷贝整个数组,无论在时间上还是在内存空间上的开销都可能是非常大的。而且在绝大部分情况下,你其实并不需要整个数组的拷贝,你只想告诉函数在那一时刻对哪个特定的数组感兴趣。

第十章 再论指针

  • 向函数传递一个一维数组
    在C语言中,任何一维数组均可以作为函数的实参。形参被改写为指向数组第一个元素的指针,所以需要一个约定来提示数组的长度。一般有两个基本方法:
    • 增加一个额外的参数,表示元素的数目( argc就是起这个作用)。
    • 赋予数组最后一个元素一个特殊的值,提示它是数组的尾部(字符串结尾的‘0’字符就是起这个作用)。这个特殊值必须不会作为正常的元素值在数组中出现。
  • 使用指针创建和使用动态数组
    它的基本思路就是使用malloc()库函数(内存分配)来得到一个指向一大块内存的指针。然后,像引用数组一样引用这块内存,其机理就是一个数组下标访问可以改写为一个指针加上偏移量。
    我们真正需要实现的是使表具有根据需要自动增长的能力,这样它的惟一限制就是内存的总容量。如果你不是直接声明一个数组,而是在运行时在堆上分配数组的内存,这可以实现这个目标。有一个库函数realloc(),它能够对一个现在的内存块大小进行重新分配(通常是使之扩大),同时不会丢失原先内存块的内容。当需要在动态表中增长一个项目时,可以进行如下操作:
    • 对表进行检查,看看它是否真的已满。
    • 如果确实已满,使用realloc()函数扩展表的长度。并进行检查,确保 realloc()操作成功进行。
    • 在表中增加所需要的项目。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值