第三章 :语义“陷阱”

3.1 指针与数组

(1)C语言中的数组值得注意的地方有以下两点:

a. C语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来。然而,C语吉中数组的元素可以是任何类型的对象,当然也可以是另外一个数组。这样,要“仿真”出一个多维数组就不是难事。

b. 对于一个数组,我们只能够做两件事:确定该数组的大小,以及获得指向该数组下标为0的元素的指针。其他有关数组的操作,哪怕它们乍看上去是以数组下标进行运算的,实际上都是通过指针进行的。换句话说,任何一个数组下标运算都等同于一个对应的指针运算,因此我们完全可以依据指针行为定义数组下标的行为。

考虑下面例子的描述方法:

int calendar [12][31] ;

这个语句声明了calendar是一个数组,该数组拥有12个数组类型的元素,其中每个元素都是一个拥有31个整型元素的数组。

Calendar是一个指向数组的指针。

(2)如果两个指针指向的是同一个数组中的元素,可以把这两个指针相减。

(3)a 和 & a

&a是一个指向数组的指针,而a是一个指向整型变量的指针,它们的类型不匹配。

(4)除了a被用作运算符sizeof 的参数这一情形(整个数组a的大小),在其他所有的情形中数组名a都代表指向数组a中下标为0的元素的指针。

(5)

(6)清空calendar[12][31]数组

a. 数组遍历

b. 指针遍历

3.2 非数组的指针

我们如何借助strcpy和strcat将两个字符串(s和t)放到一个字符串(r)里面呢?

(1)不能确定r指向何处;r所指向的地址处有足够的内存空间。

(2)不够确保r足够大。

(3)使用malloc和strlen解决问题

​​​​​

注意以下三点:

  1. malloc函数有可能无法提供请求的内存(返回空指针);
  2. 给r分配的内存在使用完之后应该及时释放(本例存储局部变量,及时释放);
  3. 作为结束标志的空字符并未计算在内。

3.3 作为参数的数组声明

(1)C语言中会自动地将作为参数的数组声明转换为相应的指针声明。

完全相同的写法:

(2)C程序员经常错误地假设:在其他情形下也会有这种自动地转换。

这两句有天壤之别:

如果一个指针参数并不实际代表一个数组,即使从技术上而言是正确的,采用数组形式的记法经常会起到误导作用

如果一个指针参数代表一个数组,情况又是如何呢?一个常见的例子就是函数main 的第二个参数:

需要注意的是,前一种写法强调的重点在于argv是一个指向某数组的起始元素的指针,该数组的元素为字符指针类型。因为这两种写法是等价的,所以读者可以任选一种最能清楚反映自己意图的写法

3.4 避免“举隅法”

(1)举隅法:以含义更宽泛的词语来代替含义相对较窄的词语,或者相反。

(2)C语言中一个常见的“陷阱”:混淆指针与指针所指向的数据。对于字符串的情形,编程者更是经常犯这种错误。例如:

某些时候可能认为,上面的赋值语句使得 p的值就是字符串"xyz"。实际上,p的值是一个指向 由’x’、’y’、’z’、’\0’ 4个字符 组成的数组的起始元素的指针

因此,如果我们执行下面的语句:

p和q现在是两个指向内存中同一地址的指针。这个赋值语句并没有同时复制内存中的字符。如下图:

因此,当我们执行完下面的语句之后:

q所指向的内存现在存储的是字符串”xYz”。因为p和q所指向的是同一块内存,所以p指向的内存中存储的当然也是字符串”xYz”。

(3)译注: ANSIC标准中禁止对string literal 作出修改。K&RC中对这一问题的说明是,试图修改字符串常量的行为是未定义的。某些C编译器还允许q[1]=’Y’这种修改行为,如 LCC v3.6。但是,这种写法不值得提倡,很多编译器已经不支持了

3.5 空指针并非空字符串

除了一个重要的例外情况 - 在C语言中将一个整数转换为一个指针,最后得到的结果都取决于具体的C编译器实现。

这个特殊情况就是常数0,编译器保证由0转换而来的指针不等于任何有效的指针。出于代码文档化的考虑,常数0这个值经常用一个符号来代替:

​​​​​​​

当然无论是直接用常数0,还是用符号NULL,效果都是相同的。

需要记住的重要一点是,当常数0被转换为指针使用时,这个指针绝对不能被解除引用(dereference)

换句话说,当我们将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容。下面的写法是完全合法的:

的行为也是未定义的。而且,与此类似的语句在不同的计算机上会有不同的效果

3.6 边界计算与不对称边界

(1)栏杆错误也叫差一错误,避免它的两个通用原则:

① 首先考虑最简单情况下的特例,然后将得到的结果外推,这是原则一。

② 仔细计算边界,绝不掉以轻心,这是原则二。

(2)遍历数组时,容易产生的边界问题

x>=16且x<=37,有多少元素呢?非常难算,不推荐这种写法。

x>=16且x< 38, 有多少元素呢?38 – 16 = 22个。

以上两种写法,在使用for循环遍历数组时,后者容错率更高。

3.7 求值顺序

(1)C语言中只有四个运算符(&&、|| 、?:和 , )存在规定的求值顺序。

(2)

① 运算符&&和运算符ll首先对左侧操作数求值,只在需要时才对右侧操作数求值。

② 运算符?:有三个操作数:在a?b:c中,操作数a首先被求值,根据a的值再求操作数b或c的值。

③ 逗号运算符,首先对左侧操作数求值,然后该值被“丢弃”,再对右侧操作数求值。

(3)C语言中其他所有运算符对其操作数求值的顺序是未定义的。特别地,赋值运算符并不保证任何求值顺序

下面这种从数组x中复制前n个元素到数组y中的做法是不正确的,因为它对求值顺序作了太多的假设:

i = 0;

while (i < n)

y[i] = x[i++];

问题出在哪里呢?上面的代码假设y[i]的地址将在i的自增操作执行之前被求值,这一点并没有任何保证!在C语言的某些实现上,有可能在i自增之前被求值;而在另外一些实现上,有可能与此相反。同样道理,下面这种版本的写法与前类似,也不正确:

i = 0;

while (i < n)

y[i++] = x[i];

另一方面,下面这种写法却能正确工作

i = 0;

while (i < n)

{

y[i] = x[i];

i++;

}

3.8 运算符&&、|| 和 !

C语言中有两类逻辑运算符,某些时候可以互换:按位运算符&、l和~,以及逻辑运算符&&、|| 和 !。

如果程序员用其中一类的某个运算符替换掉另一类中对应的运算符,他也许会大吃一惊:互换之后程序看上去还能“正常”工作,但是实际上这只是巧合所致。

3.9 整数溢出

C语言中存在两类整数算术运算,有符号运算与无符号运算。

在无符号算术运算中,没有所谓的“溢出”一说:所有的无符号运算都是以2的n次方为模,这里n是结果中的位数。

如果算术运算符的一个操作数是有符号整数,另一个是无符号整数,那么有符号整数会被转换为无符号整数,“溢出”也不可能发生。

当两个操作数都是有符号整数时,“溢出”就有可能发生,而且 “溢出”的结果是未定义的。当一个运算的结果发生“溢出”时,做出任何假设都是不安全的。

3.10  为函数main提供返回值

第四章 :连接

4.1 什么是连接器?

(1)连接器与编译器的配合

C语言中的一个重要思想就是分别编译(Separate Compilation),即若干个源程序可以在不同的时候单独进行编译,然后在恰当的时候整合到一起。

连接器理解机器语言和内存布局。编译器的责任是把C源程序“翻译”成对连接器有意义的形式,这样连接器就能够“读懂”C源程序了。

典型的连接器把由编译器或汇编器生成的若干个目标模块,整合成一个被称为载入模块或可执行文件的实体,该实体能够被操作系统直接执行。其中,某些目标模块是直接作为输入提供给连接器的;而另外一些目标模块则是根据连接过程的需要,从包括有类似printf函数的库文件中取得的。

(2)两个文件的同名函数,连接器区分处理方式

连接器通常把目标模块看成是由一组外部对象(external object)组成的。每个外部对象代表着机器内存中的某个部分,并通过一个外部名称来识别。因此,程序中的每个函数和每个外部变量,如果没有被声明为static,就都是一个外部对象。

大多数连接器都禁止同一个载入模块中的两个不同外部对象拥有相同的名称。然而,在多个目标模块整合成一个载入模块时,这些目标模块可能就包含了同名的外部对象。连接器的一个重要工作就是处理这类命名冲突。

处理命名冲突的最简单办法就是干脆完全禁止。对于外部对象是函数的情形,这种做法当然正确,一个程序如果包括两个同名的不同函数,编译器根本就不应该接受。而对于外部对象是变量的情形,问题就变得有些困难了。不同的连接器对这种情形有着不同的处理方式,我们将在后面看到这一点的重要性。

(3)连接器的工作情形

有了这些信息,我们现在可以大致想像出连接器是如何工作的情形了。连接器的输入是一组目标模块和库文件。连接器的输出是一个载入模块。连接器读入目标模块和库文件,同时生成载入模块。对每个目标模块中的每个外部对象,连接器都要检查载入模块,看是否已有同名的外部对象。如果没有,连接器就将该外部对象添加到载入模块中;如果有,连接器就要开始处理命名冲突。

除了外部对象之外,目标模块中还可能包括了对其他模块中的外部对象的引用。例如,一个调用了函数printf 的C程序所生成的目标模块,就包括了一个对函数printf 的引用。可以推测得出,该引用指向的是一个位于某个库文件中的外部对象。在连接器生成载入模块的过程中,它必须同时记录这些外部对象的引用。当连接器读入一个目标模块时,它必须解析出这个目标模块中定义的所有外部对象的引用,并作出标记说明这些外部对象不再是未定义的。

因为连接器对C语言“知之甚少”,所以有很多错误不能被检测出来。再次强调,如果读者的C语言实现中提供了lint程序,切记要使用!

4.2 声明与定义

出现在另一个源文件中,大多数系统都会拒绝接受该程序。但是,如果一个外部变量在多个源文件中定义却并没有指定初始值,那么某些系统会接受这个程序,而另外一些系统则不会接受。要想在所有的C语言实现中避免这个问题,唯一的解决办法就是每个外部变量只定义一次。

4.3 命名冲突与static修饰符

为了避免可能出现的命名冲突,如果一个函数仅仅被同一个源文件中的其他函数调用,我们就应该声明该函数为static。

static修饰符是一个能够减少此类命名冲突的有用工具。

4.4 形参、实参与返回值

​​​​​​​​​​​​​​

因为整数所占的存储空间要大于字符所占的存储空间,所以字符c附近的内存将被覆盖。

字符c附近的内存中存储的内容是由编译器决定的,本例中它存放的是整数i的低端部分。因此,每次读入一个数值到c时,都会将i的低端部分覆盖为0,而i的高端部分本来就是0,相当于i每次被重新设置为0,循环将一直进行。当到达文件的结束位置后,scanf函数不再试图读入新的数值到c。这时,i 才可以正常地递增,最后终止循环。

4.5 检查外部类型

假定我们有一个C程序,它由两个源文件组成。一个文件中包含外部变量n的声明:

extern int n;

另个文件中包含外部变量n的定义:

long n;

因此,保证一个特定名称的所有外部定义在每个目标模块中都有相同的类型,一般来说是程序员的责任。而且,“相同的类型”应该是严格意义上的相同

例如,考虑下面的程序,在一个文件中包含定义:

char filename [ ] = "/etc/passwd" ;而在另一个文件中包含声明:

extern char* filename;

        尽管在某些上下文环境中,数组与指针非常类似,但它们毕竟不同。在第一个声明中,filename是一个字符数组的名称。尽管在一个语句中引用filename的值将得到指向该数组起始元素的指针,但是filename的类型是“字符数组”,而不是“字符指针”。在第二个声明中,filename被确定为一个指针。这两个对filename的声明使用存储空间的方式是不同的;它们无法以一种合乎情理的方式共存。

4.6 头文件

每个外部对象只在一个地方声明。这个声明的地方一般就在一个头文件中,需要用到该外部对象的所有模块都应该包括这个头文件。特别需要指出的是,定义该外部对象的模块也应该包括这个头文件。

4.7 练习

练习4-1  假定一个程序在一个源文件中包含了声明:long foo;

而在另一个源文件中包含了:

extern short foo;

又进一步假定,如果给long类型的foo赋一个较小的值,例如37,那么short类型的foo就同时获得了一个值37。我们能够对运行该程序的硬件作出什么样的推断?如果 short类型的foo 得到的值不是37而是0,我们又能够作出什么样的推断?

如果把值37赋给long 型的 foo,相当于同时把值37也赋给了short型的foo,那么这意味着short型的foo,与 long型的foo中包含了值37的有效位的部分,两者在内存中占用的是同一区域。这有可能是因为 long型和 short型被实现为同一类型,但很少有C语言实现会这样做。更有可能的是,long型的foo的低位部分与short型的foo共享了相同的内存空间,一般情况下,这个部分所处的内存地址较低;因此我们的一个可能推论就是,运行该程序的硬件是一个低位优先(little-endian〉的机器。同样道理,如果在long 型的 foo中存储了值37,而 short型的 foo 的值却是0,我们所用的硬件可能是一个高位优先( big-endian)的机器。

C陷阱与缺陷(二)语义“陷阱”、连接相关推荐

  1. 《C陷阱与缺陷》----词法“陷阱”

    导言: 由于一个程序错误可以从不同层面采用不同方式进行考察,而根据程序错误与考察程序的方式之间的相关性,可以将程序错误进行划分为各种陷阱与缺陷: ①.词法"陷阱" ②.语法&quo ...

  2. C陷阱与缺陷之词法陷阱

    该文章及后续文章均为阅读<C陷阱和缺陷>后的读数笔记,方便以后回顾 C陷阱和缺陷电子版图书下载地址:点击打开链接 第一章词法陷阱 1.1 = 不同于 == 在C语言中,符号=作为赋值运算符 ...

  3. 《C陷阱与缺陷》词法陷阱-贪心法

    C语言的某些符号,例如/ .* .和=,只有一个字符长,称为单字符符号.而C语言中的其他符号,例如/ 和 = = ,以及标识符,包括了多个字符,称为多字符符号.当C编译器读入一个字符'/'后又跟了一个 ...

  4. c缺陷与陷阱 第3章 语义陷阱

    1. 指针与数组 c语言中的数组要注意以下两点: 下面的方法存在问题,因为数组的大小必须在编译期作为一个常数确定下来. a[3][4] 本质也是一个一维数组(包含3个元素),只不过一维数组中的元素是也 ...

  5. 【C陷阱与缺陷】----语法陷阱

  6. 《C陷阱与缺陷》一导读

    前 言 C陷阱与缺陷 对于经验丰富的行家而言,得心应手的工具在初学时的困难程度往往要超过那些容易上手的工具.刚刚接触飞机驾驶的学员,初航时总是谨小慎微,只敢沿着海岸线来回飞行,等他们稍有经验就会明白这 ...

  7. 《C陷阱与缺陷》第三章

    文章目录 前言: 语义"陷阱" 指针与数组 操作符:sizeof() 指针 非数组的指针 作为参数的数组声明 避免"举隅法" 空指针并非空字符串 边界计算与不对 ...

  8. 《C陷阱与缺陷》----第三章 语义陷阱

    第三章. 语义陷阱 3.1 指针与数组 3.2 非数组的指针 3.3 作为参数的数组声明 3.4 空指针并非空字符串 3.5 边界计算与不对称边界 3.6 求值顺序 3.9 整数溢出 3.10 为函数 ...

  9. 阅读《C陷阱与缺陷》的知识增量

    看完<C陷阱与缺陷>,忍不住要重新翻一下,记录一下与自己的惯性思维不符合的地方.记录的是知识的增量,是这几天的流量,而不是存量. 这本书是在ASCI C/C89订制之前写的,有些地方有疏漏 ...

  10. C陷阱与缺陷--笔记

    原文链接:http://codeshold.me/2017/01/c_trapsandpitfalls.html 词法陷阱 语法陷阱 语义陷阱 连接 库函数 预处理器 可移植性缺陷 附录 补充知识点 ...

最新文章

  1. r语言ggplot怎么把多个维度数据合并在一个图中表示_R语言作图——Histogram
  2. 胡说八道之贝尔曼最优解
  3. IdentityServer4 配置负载均衡
  4. 用Priam设置Cassandra
  5. Source Insight使用技巧
  6. sqlserver agent不能启动
  7. 【语音去噪】基于matlab GUI傅立叶变换语音降噪混频【含Matlab源码 297期】
  8. 基于大数据的城市租房信息可视化分析系统
  9. stm32 ov2640硬件软件笔记
  10. wps计算机打印双面输出,如何在电脑wps软件内设置双面打印
  11. matlab 找到数组中第一个不连续点_超全Matlab绘图方法整理
  12. ensp VRRP配置2
  13. C++ 野指针和悬空指针
  14. ssm项目之第三方QQ登陆
  15. UE4中实现Cesium 3dtileset的压平
  16. BlackHat DEFCON现场秀:阿里安全专家演示“视频水印叠加”和“一分钟越狱iOS 11.4”...
  17. 多项式定理【OI Pharos 6.2.2】
  18. TFTP服务器与客户端的安装
  19. Python安装Pyinstaller失败,Preparing wheel metadata ... error
  20. ctfshow misc2 软盘

热门文章

  1. DevOps 实践指南
  2. 网络安全宣传周:强调个人信息保护│预防个人信息泄露实操指南
  3. AAAI2021论文列表(中英对照)
  4. python将变量写入文件_python 如何把变量写入文件
  5. OSG绘制空间凹多边形以及透明效果的实现
  6. 医疗行业软件开发流程-立项阶段
  7. Delphi下使用并口控制打印机的方法
  8. matlab 求虚数的反正切,matlab中的反正切函数
  9. sql语句练习题(mysql版)
  10. 生产排程系统_APS(高级计划排产)系统该如何选型,主要从哪些方面考虑?