文章目录
下面整理的是《C专家编程》这本书中提到的一些C语言的缺陷的存在争议的问题。其中罗列的程序都是经过调试运行的,《C专家编程》中提到的一些bug或者缺陷若已经在本人使用的C版本中得到修复将不再重述,只会简略提到。书中提到的很多问题都是在没有一款强的IDE的情况下,现如今我们的IDE足够强大很难产生作者提到的很多问题,但了解这些不无裨益。
环境:windows7旗舰版
编译器:gcc (i686-posix-dwarf-rev1, Built by MinGW-W64 project) 4.9.2
cl 来源于Microsoft Visual Studio 2013
1 C穿越时空的迷雾
1.1 const类型变量
const变量的声明在C语言中命名为常量,但是实现的功能仅仅是不能被直接的修改,可以通过指针寻址修改const的值。
1.1.1 不可改变的值
如下代码:
const int num = 12;
int array[num];
在gcc下编译可以正常运行,而且可以作为正常的数组使用,但是在cl中就会出现如下的错误提示:
说明const变量作为数组的初始化长度在不同的编译器中有不同的定义。虽然const为不可改变的变量,但是通过读取内存地址可以间接的修改const的值。
const int num = 12;
int* ptr = #
printf("const num:%d\n", num);
*ptr = 24;
printf("const num:%d\n", num);
代码的编译和执行在cl和gcc下基本相同输出结果为:
说明const这个类型并不是真正的const,(C++中对const的定义很严格以上情况不会发生)但是庆幸的是在gcc中编译给出了警告:
这可以让开发人员注意到这一点(突然感觉gcc如此强大)
1.1.2 const类型匹配
书中提到的类型匹配问题在一级指针和普通变量中不存在,但在多级指针中cl无反应,gcc会警告用户。
void run(const char **num)
{
printf("run function!\n");
}
int main(void)
{
char* a[] = { "12" };
const int num = 12;
run(a);
system("pause");
return 0;
}
gcc警告:
1.2 隐类型隐式转换
C语言中存在数据类型的转换例如int转换为float,float转换为double,unsigned 转换为signed,这些数据类型再进行计算是会进行隐式转换,对计算来说是好事,比如:
int num_rst = 2;
double num_snd = 3.0;
double result = num_rst / num_snd;
当进行计算时,num_rst先隐式转换为double再参与计算,省去了程序员的一些工作。但是有些数据类型转换就会产生错误。
1.2.1 unsigned和signed之间的转换
unsigned short a = -1;
if (a < 1)
{
printf("-1 < (unisgned int)1 \n");
}
else
{
printf("(unsigned) 1:%d\n", (unsigned)1);
printf("(unsigned)-1:%d\n", a);
printf("-1 > (unisgned int)1 \n");
}
cl和gcc下输出的结果都是:
可以看出输出结果和我们想象的有差距。
并且当short类型换成char类型使输出为255,而当换成int是输出为-1,其实利用负数在内存中的状态来思考就很好理解了,负数在内存中以补码的方式存在,char类型为一个字节八位,-1的补码为1111 1111直接解析就是255;short int为2个字节十六位-1的补码为1111 1111 1111 1111直接解析就是65535 int类型为4个字节32位,-1的补码为0xFFFFFFFF结果是4294967295打打印出来是-1这和printf函数有关,将%d改为%ud即可。可见unsigned 类型对变成有一定的影响,所以尽量不要使用unsigned,除非一些要用二进制位段的地方,比如51单片机等。
1.3 关于pragma的使用
gcc和cl不会出现异常。
2 这不是BUG而是语言特性
正如本章的题目所示,文中提到的很多并不是bug而是语言特性,只要程序员稍加注意就没有问题。
2.1 switch的标注
书中提到的容易出错的问题,我并不认为是问题,但在当case之间不能再break的时候加上/fall through/的注释很有利于检查:
switch (num)
{
case 1:printf("");
/*fall through*/
case 2:printf("");
case 3:printf("");
}
2.2 sizeof歧义
int *p = 12;
int a = 12 * sizeof * p;
printf("%d\n", a);
这种地方sizeof后面的’*’到底解析为乘号还是取值符,这个并不算很有争议,毕竟sizeof必须跟参数但是:
int apple = sizeof(int)*p
有两种解释方法:
- 第一种:sizeof(int) * p中*是一个乘号;
- 第二种:解析为sizeof((int)p)先对p进行类型转换在计算sizeof。
可能我们第一眼感觉都是第一种解析,但第二种解析方式并没有出错,并不能以我们看代码的方向来思考编译器解析代码的规则,在我电脑上的编译器会根据p的数据类型选择如何解析,这对程序员来说是好事,但我们还是必须知道这些缺陷。
解决类似问题也很简单就是多加几个括号即可。
2.3 优先级
相关优先级如下:
建议:
- 乘法和除法优先级高于加法和减法;
- 当设计多种操作符时加上括号绝对不会错。
赋值符就有右结合性。
int a, b = 2, c = 4;
a = b = c;
先执行b = c再执行a = b;
逻辑操作符具有左结合性。
int a = 1, b = 0, c = 0;
printf("%d\n", a | b | c);
先执行a|b在执行|c。
2.4 gets漏洞
gets函数可以将用户输入写入对应的堆栈中,但不会对用户输入进行检查,当用户输入的字符数量超过指定区域时他照样会写入,这就导致了内存越界的问题。
char buf[10];
gets(buf);
printf("%p\n", buf);
printf("%s\n", buf);
这将导致很严重的内存问题,可以使用gets或者fget(buf,sizeof(buf),stdin)来代替输入,vs中对于使用gets除非定义#define _CRT_SECURE_NO_WARNINGS将会报错,而gcc中仍然可以使用。
2.5 局部变量返回问题
char buf[12];
…
return buf;
这段代码没有语法错误但会导致一个问题buf的内存是一个局部的当函数执行结束时buf也会被自动销毁,所以返回的指针也就没有了意义。
解决方法:
3 分析C语言的声明
3.1 C语言复杂的声明
int (*fun())(void) //函数返回值是一个函数指针
int (*fun(void))[] //函数返回值是一个指向数组的指针
int(*fun[])() //函数指针数组
类似的形式判断规则:
- A. 声明从它的名字开始读取,然后按照优先级顺序依次读取;
- B. 优先级从高到低依次是:
- B1:声明中被括号扩住的部分;
- B2:后缀操作符()表示一个函数 []表示一个数组;
- B3:前缀操作符,*表示”指向…的指针”;
- C. 如果const,volatile后面紧跟类型说明符(如int),则他做用于类型说明符,否则作用于它左边紧邻的指针星号。
4令人震惊的事实:数组和指针并不相同
数组和指针在某些方面很相似但两者不同:
double* ptr = 1.3;
ptr需要指向一个内存空间,但1.3没有内存空间 。
5 对链接的思考
5.1 链接库
静态链接:每个可执行文件都拥有一份对应代码二进制的拷贝,导致可执行文件的大小相对较大。
动态连接:所有可执行文件都共享同一个链接库,可执行文件相对较小,但是对文件的搜寻会占用一定的时间,但相对于静态链接来说速度上不会太差,而且动态连接不用依赖于操作系统的版本问题,独立性更强。
以后写相对大一点的项目都建议使用动态连接库。
注:
- 动态链接库的可扩展名(linux下)为”.so”(shared object),静态链接库的扩展名为”.a”(archive);window下动态链接库是”dll”(dynamic link library),静态链接库是”.lib”(libray),
- 使用的动态链接库是libname.so,连接时可以使用-lname即可找到对应的库文件
- 在使用动态链接库的同时尽量使用确定的目录来通知编译器
- C语言中头文件的名称并一定和链接库的名称吻合
- 静态链接库和动态链接库符号提取方法不同,静态链接更为蛋疼,连接顺序的不同可能导致程序的错误。(始终将-l函数库选项放在变异命令的最右边是不错的选择)
5.2 警惕Interpositioning
Interpositioning指的是用户自定义的函数使用的函数名和库函数中的函数名相同导致的bug,但是现如今在gcc和cl中都会提示错误所以基本不存在类似问题。
Interpositioning补充:
形成bug的主要原因是用户自定义的一个函数和库函数名称冲突,但是用户调用的另一个库函数中用到了被用户覆盖的库函数,导致函数执行出错。
6 运动的诗章:运行时数据结构
a.out由来是assember output(汇编程序输出)的缩写Unix中#define FS_MAGIC 0x011954是该文件系统设计者的生日。
6.1 a.out的组成结构
a.out由三个段组成:文本段,数据段和bss段。
bss保存未经初始化的变量,经初始化的静态变量和全局变量会存储在数据段,执行代码会存储在文本段。
图中a.out比exe多一个int类型的全局变量,a比a.out多一个int类型的局部变量,可以看出局部变量并不占用空间,局部变量是在代码运行期间分配内存,局部变量存储在堆栈区。
6.2 线程控制(setjmp和longjmp的使用)
setjmp和longjmp类似于C++中的try和catch,头文件是setjmp.h。
longjmp可以从函数中跳出而goto不能。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <setjmp.h>
jmp_buf jmp;
void fun()
{
printf("in the function before jmp\n");
longjmp(jmp, 2);
printf("in the function after jmp\n");
}
int main(int argc,char* argv[])
{
printf("before set jmp\n");
//第一次初始化完成的返回值为0,跳转成功后的返回值为longjmp的第二个参数
int ret = setjmp(jmp);
printf("%d\n", ret);
printf("after set jmp\n");
if (2 == ret)
{
printf("跳转成功\n");
system("pause");
return;
}
fun();
system("pause");
return 0;
}
linux下C语言工具。
之前是查看源代码的程序,dis之后是检查可执行文件的程序,truss之后是帮助调试的程序,collector之后是性能优化的辅助工具。
7 对内存的思考
MS-DOS总共1MB的内存但却限制任何应用程序使用内存不能大于640KB,原因是部分作保留由系统使用。
可用near,_far,huge修饰指针来控制指针长度,但vs貌似不支持。
7.1 内存错误
内存损坏:释放或者改写正在使用的内存块。
内存泄露:未释放不再使用的内存块。
检测内存泄露:
- 使用
swap
命令检测内存系统使用情况; - 使用``ps -lu [用户名]```检查用户进程内存分配。
总线错误(bus error(core dumped)):多产生于未对齐的读或者写。
注:但是在vs和ubuntu中均未出现总线错误的提示。
段错误(segmentation fault(core dumped))
引起段错误的直接原因:
- 解除引用一个包含非法值的指针;
- 解除引用一个空指针(常常为系统返回,未做检查);
- 未得到正确权限进行访问。如:向制度文本存储值;
- 用完了堆栈或者堆。
显示出现段错误,0地址无法写:
8 程序员为什么无法愤青万圣节和圣诞节
万圣节:11月1日,万圣节前夜是10月30日,十月的英文是October(Oct),八进制。
圣诞节是在12月25日,12月的英文是(Dec),十进制。
八进制的30刚好是十进制的25,这里作者提到的更像是一个冷笑话!
9 再论数组
记住:
- 数组是一种类型;
- 数组名并不是指针;
- 当数组作为函数参数时,数组会退化为指针;
- C语言不存在多维数组,C语言支持的只是数组的数组;
- C语言中任何数组在内存中的映射都是线性的。
10 再论指针
多维数组的存储结构都是线性,只是解析方式不同而已:
所有有效的传递:
void fun_rst(int buf[2][2][2])
{}
void fun_snd(int buf[][2][2])
{}
void fun_thd(int (*buf)[2][2])
{}
int main(int argc,char* argv[])
{
int buf[2][2][2];
int(*p)[2][2][2] = &buf;
fun_rst(buf);
fun_snd(buf);
fun_thd(buf);
fun_rst(*p);
fun_snd(*p);
fun_thd(*p);
system("pause");
return 0;
}
11 附录
1、判断一个链表中是否存在环:
- 对访问过的每个元素进行标记,之后若标记重复则有环;
- 将访问过的数据存入一个数组,若之后访问的数据在数组中则有环(数据可以重复啊!!!);
- 创建一个指针指向链表头,然后连续访问n个元素并与指针所指向的元素进行比较,比较结束后将指针向后移动,若出现相等则有环。
答案:快慢指针。
2、库函数和系统函数的区别
3、如何确定一个变量是有符号还是无符号:
分析:决不能使用函数调用,函数调用就会发生类型转换
- 若判断的是一个变量:
#define GO(a) (a >= 0 && ~a>=0)
- 若判断的是一个类型:
#define GO(type)((type)0 - 1 > 0)