C陷阱和缺陷整理四

1.assert宏的定义

#define assert(e) \

((void)((e) || _assert_error(__FILE__, __LINE__)))

库里面对这个宏做了这样的定义,当宏参数(或表达式)e为真的时候由||运算符的运算规则会执行_assert_error(__FILE__, __LINE__)从而打印一条报警信息。所以整个表达式的最终会变为(void)0或者(void)1这种形式,这种形式确实有点奇怪?

系统这样定义的目的是当一个值被转换为void类型之后,没有一个类型的变量可以接收这个值,因为C语言中不能定义一个void类型的变量,所以这样可以防止(void)0或者(void)1被作为右值赋给其他变量。

2.宏定义的危险

虽然宏定义可以为我们提供很多方便,但是其中也可能隐藏着许多bug,现在C语言中的typedef完全可以替代宏定义的功能,同时具有更高的安全性,所以尽量使用typedef来替代#define宏定义。看下面的例子:

#define T1 struct foo *

typedef struct foo *T2;

从上面这两个定义来看,T1和T2从概念上来说完全相同,都是指向foo的指针,但是当我们用他们来定义多个变量时,宏定义就会出现问题:

T1 a, b;

T2 a, b;

第一个定义语句会被展开为:

struct foo *a, b;

然而第二个语句却等价于:

struct foo *a, *b;

3.字符型像整型提升

如果编程者关注一个最高位是1的字符其数值究竟是正还是负,可以将这个字符声明为无符号字符(unsigned char)。这样,无论是什么编译器,在将该字符转换为整数时都只需要将多余的位填充0即可。而如果声明为一般的字符变量,那么在某些编译器上可能会作为有符号数处理,而在另一些编译器上又会作为无符号数处理。看下面一个例子:

#include <stdio.h>

int main(void)

{

char i = -1;

unsigned int j = (unsigned int)i;

unsigned int k = (unsigned char)i;

printf("%u %u", j, k);

}

这个例子的输出是多少呢?

分析:在我的PC上面输出为4294967295 255.现在来分析一下其中的原因,首先需要知道的是强制类型转换的过程是首先将单元中的值取出来,然后按照需要转换的类型进行一定的扩展,最终得到强制转换类型数据。上述代码中将i设置为-1,在二进制上面的8个bits的表示方式为全1,而我的编译器将这8位数值当做有符号数处理,所以强制类型转换的时候分别会被扩展为32位的有符号数和8位的有符号数,最后再按照无符号数解释这个值,所以最终的打印结果是int类型无符号最大值和unsigned char类型无符号最大值。

4.移位运算

(1)对于无符号数的左移或者右移都是在移入的位置填充0,所以在该无符号数表示的范围内,都相当于乘2或者除2的操作。所以一个非负的数据除2的运算完全可以使用右移一位来替代,且替代之后执行效率更高。

(2)对于有符号数来说,左移依旧是填入0,而右移大多数编译器都会填充该数的符号位。所以有符号数的右移是不能够看做除2运算的,而有符号数的左移是可以看做乘2运算的。例如:

int i = -1;

printf("%d ", i>>1);

程序段的输出一般都为-1,而并不是像C语言实现i /= 2那样的结果为0。因为在C语言实现i /= 2的时候,先进行纯值的运算,即先算的是 1/2的值为0,最后再为其添加负号。

(3)移位长度的限制,一般来说,都需要严格要求移位长度小于等于该类型的长度,因为这样硬件上就可以高效的实现移位运算。

(4)如果你写出下面这样的程序:

int i = 1;

i >>= -2;

不要期望能够实现你所期望的功能,虽然i >>= -2这样的表达式很多编译器不会报错,但是编译器也不会真的理会,只是会默默丢弃这条语句。

5.内存位置0

NULL指针并不指向任何对象,因此,除非是用于赋值或者比较运算,出于其他任何目的使用NULL指针都是非法的。

在其他非法情况下究竟会得到什么结果呢?不同的编译器会有不同的结果。一些C语言实现对内存位置0强加了硬件级的读保护,在其上工作的程序如果错误的使用了一个NULL指针,将立即终止其执行。其他一些C语言实现对内存位置0只允许读,不允许写,在这种情况下,一个NULL指针似乎指向的是某个字符串,但其内容通常不过是一堆“垃圾信息”。还有一些C语言实现对内存位置0既允许读,也允许写,在这种实现上面工作的程序如果错误的使用了一个NULL指针,和可能覆盖了操作系统的那部分内容,造成彻底的灾难。

6.printf族函数

printf、fprintf和sprintf的返回值都是已传送的字符数。对于sprintf的情形,作为输出数据结束标志的空字符并不计入总的字符数。如果printf或fprintf在试图写入时出现一个I/O错误,将返回一个负值。在这种情况下,我们就无从得知究竟有多少字符已经被写出。因为sprintf函数并不进行I/O操作,因此它不会返回负值。当然,也不排除有的C语言实现会因为某种原因,而令sprintf函数返回一个负值。

因为格式字符串决定了其余参数的类型,而且可以到运行时才建立格式字符串,所以C语言实现要检查printf函数的参数类型是否正确是异常困难的。下面的程序:

printf("%d\n", 0.1);

最后得到的结果可能毫无意义,而且在程序运行之前,这些错误很难被编译器检测到,成为漏网之鱼。

7.使用varargs.h来实现可变参数列表

varargs.h头文件中定义了宏名va_list, va_dcl, va_start, va_end以及va_arg。然而va_alist一般由编程者来定义,注意却分va_list和va_alist。

对于可变参数列表的第n个参数,在已知其类型的情况下,要对其进行存取还需要一些额外的信息。这些信息是通过已经存取的第一个参数到第n-1个参数而间接得到的,可以把这些信息看做是一个指向参数列表内部的指针。

这些信息存储在一个类型为va_list的对象中,因此,当我们声明了一个名称为ap的类型为va_list的对象后,只需要给定ap的第一个参数的类型就可以确定第1个参数的值。通过va_list存取一个参数之后,va_list将被更新,指向参数列表中的下一个参数,va_list中包括了存取全部参数的所有必要信息。

ANSI中的stdarg.h完成varargs.h的功能,使用stdarg.h实现的printf函数如下:

#include<stdarg.h>

int printf(char *format, ...)

{

va_list ap;

int n;

va_start(ap, format);

n = vprintf(format, ap);

va_end(ap);

return n;

}

基本思想:printf函数接收的第一个参数固定是一个字符指针,后面的参数不固定。在调用printf函数的时候,一定会生成一个参数列表存放在内存中某个位置,然后通过va_start找出字符串后面的真正的值部分的起始地址,然后调用vprintf函数输出,vprintf函数和printf函数类似,只不过它会将格式输出符替换为后面的参数。vprintf函数相比于printf函数参数更为具体。在得到真实的值的地址之后,通过格式输出符每次使用相应的指针强制取出相应类型的值,然后将指针后移,下次再将指针强制转换为另一个类型,取出另一个类型的值,这就是参数输出的过程。这个过程必须要保证参数值在内存中是连续存放的才能够成功。同时需要注意的是,参数类型的提升:char、short会被强制提升为int,float会被强制提升为double,所以从参数值地址处取出的不可能为一个char类型的参数,因为实际传输过程中这个char已经被提升为int类型,这或许也是为了程序处理的方便。本书(C陷阱和缺陷)中对于va_alist的实现以及vprintf的实现并未提及,以后有机会再做补充。

时间: 2024-10-10 22:47:06

C陷阱和缺陷整理四的相关文章

读书笔记--C陷阱与缺陷(四)

第四章 1. 连接器 C语言的一个重要思想就是分别编译:若干个源程序可在不同的时候单独进行编译,恰当的时候整合到一起. 连接器一般与C编译器分离,其输入是一组目标模块(编译后的模块)和库文件,输出是一个载入模块(执行文件). 2. 命名冲突与static修饰符 static修饰符可有效减少命名冲突! 如: static int a; 与 int a; 声明含义相同,但是前者限制a的作用域在一个源文件(.c)内,其他源文件是不可见的.但后者都是可见的会产生命名冲突. 如果若干个函数需要共享一组外部

C陷阱与缺陷整理二

1.在C语言中,我们没有办法将一个数组作为函数参数传递,如果我们使用数组名作为参数,这个时候数组名立刻会被转换为指向该数组的第一个元素的指针. 关于这一点的理解可以向前深入一步,比如定义的数组为int a[3],那么a作为参数传递之后会变为int *类型:如果定义的数组为int a[3][4],那么a作为参数传递之后被变为int (*)[4]:如果定义的数组为int a[3][4][5],那么a作为参数传递之后会变为int (*)[4][5]:后续的以此类推.为什么可以这样呢?因为C语言中的多维

C陷阱与缺陷整理一

1.词法分析中的"贪心法" C语言的某些符号,例如/.*和=,只有一个字符长,称为单字符符号.而C语言中的其他符号,例如/*和==,以及标识符等都包含了多个字符,称为多字符符号.当C编译器读入一个字符'/'后又跟了一个字符'*',那么编译器就必须做出判断:是将其作为两个分别的符号对待,还是合起来作为一个符号来对待.C语言对这个问题的解决方案可以归纳为一个很简单的规则:每一个符号应该包含尽可能多的字符.也就是说,编译器将程序分解成符号的方法是,从左到右一个字符一个字符地读入,如果该字符可

C陷阱与缺陷整理三

1.大多数C语言的实现都通过函数main的返回值来告诉操作系统该函数的执行是成功还是失败.典型的处理方案是,返回值为0代表程序执行成功,返回值非0则表示程序执行失败.如果一个程序的main函数并不返回任何值,那么有可能看上去执行失败.所以建议我们的C程序的main函数应该如下编写: int main() { return 0; } 当然如果main函数需要接受参数的话将参数声明加上更加完美. 2.一个C程序可能是由多个分别编译的部分组成,这些不同的部分通过一个通常叫做连接器的程序合并为一个整体.

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

看完<C陷阱与缺陷>,忍不住要重新翻一下,记录一下与自己的惯性思维不符合的地方.记录的是知识的增量,是这几天的流量,而不是存量. 这本书是在ASCI C/C89订制之前写的,有些地方有疏漏. 第一章 词法陷阱 1.3 C语言中解析符号时使用贪心策略,如x+++++y将被解析为x++ ++ +y,并编译出错. 1.5 单引号引起的一个字符代表一个对应的整数,对于采用ASCII字符集的编译器而言,'a'与0141.97含义一致. 练习1.1 嵌套注释(如/*/**/*/)只在某些C编译器中允许,如

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

前言和导读 "得心应手的工具在初学时的困难程度往往超过那些容易上手的工具."比较认同这句话.我至今觉得自己其实还是个刚入了门的初学者. 第一章 "词法"陷阱 由于之前学过编译原理,对编译器词法分析(主要是符号识别过程)比较了解,理解起来不困难. 在讲到"="和"=="."|"和"||"."&"和"&&"时,联想起以前见过一些

读书笔记--C陷阱与缺陷(七)

第七章 1.null指针并不指向任何对象,所以只用于赋值和比较运算,其他使用目的都是非法的. 误用null指针的后果是未定义的,根据编译器各异. 有的编译器对内存位置0只读,有的可读写. 书中给出了一种判断编译器如何处理内存0的代码: 1 #include <stdio.h> 2 int main() 3 { 4 5 char *p; 6 p=NULL; 7 printf("location 0 contains: %d\n", *p); 8 9 return 0; 10

《C陷阱与缺陷》读书笔记

<C陷阱与缺陷>读书笔记 1.编译器中的词法分析器负责将程序分解为一个个符号.C语言中,符号之间的空白 (包括Space ,Tab , Enter) 都将被忽略,但一个符号的中间不能有空白,否则可能被解释成为另一个或几个符号. 2.编译器将程序分解成符号的方法是从左到右逐个字符读入,如果该字符可能会组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分:如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已经不再可能组成一个有意义的

算法整理(四):浅析快速排序的优化问题

前文介绍了快速排序的单边扫描和双边扫描,但么有做对比,今天来简单分析下. 一.单边扫描的缺点 单边扫描最大的缺点是每次都要交换,如果一个数组是 5 4 3 2 1,用单边扫描的话,则从4开始,4要和4交换一次,3要和3交换一次,依次类推,这种无意义的操作.正因此用双边扫描会更好,第一趟只需交换一次,就能得到1 4 3 2 5这样的数组.但双边扫描也是可以进一步优化的. 二.双边扫描的优化 优化一:对key值得选取应该使用随机选取的原则,而非第一个数字.意义大家都懂得. 优化二:前文的方法是挖坑法