小心C语言的定义与声明
小心C语言的定义与声明
注:为便于说明问题,文中提及的变量和函数都被简化。
一、起源
DBProxy在测试过程中,发现对其执行某步管理操作后,程序有时会崩溃,但不是每次都出现。
二、GDB跟踪
反复多次测试,然后用GDB打开core dump文件,查看程序崩溃时的堆栈,发现可能的崩溃只有两处,这两处的共同点是前面都调用了一个函数get_pointer得到一个指针,如下图所示:
然后在使用该指针进行下步操作时程序崩溃。
查看该指针的值,发现其指向一个无效地址,所以操作该地址产生了段错误,如下图所示:
三、无效地址的产生原因
函数get_pointer的原型是char *get_pointer(void),函数体内只是通过简单的malloc操作获取一个char*类型的指针,然后返回给调用者。
malloc()结果只有两种:
- 成功,返回一个指向合法地址的指针
- 失败,返回NULL
为什么会返回一个无效地址呢?
在get_pointer函数返回前加入一个printf语句,打印即将返回给调用者的指针值:
再在调用处之后也加入一个printf语句,打印调用者接收到的指针值:
重新编译,反复进行多次测试,发现打印出的两个值有时相同有时不同,相同时程序正常运行,不同时程序必然崩溃。
更重要的是,这些值看上去很有规律:
p=1c34abd0
pointer=1c34abd0
正常运行
p=38ac7bda1690
pointer=7bda1690
接下来崩溃
p=a5ef6824
pointer=ffffffffa5ef6824
接下来崩溃
……
观察到一个现象:如果p值是4字节(即高4字节为0)且低4字节的最高位为0,则二者相同,否则二者必定不同。而pointer值的高4字节只有两种情况,一种是0×00000000,一种是0xFFFFFFFF,再结合低4字节的首位,可以看出pointer值的高4字节是由p值的低4字节补全了高4字节形成的。
四、遗漏的头文件
注意到编译时有如下信息输出“警告:初始化时将整数赋给指针,未作类型转换”。当
初察看该函数,发现返回的是指针,认为是编译器的误报,未作处理。重新审视该警告信息,结合调用处的源文件,发现该文件没有包含声明get_pointer函数的头文件。在源文件头部添加#include语句,包含该头文件后,重新编译,警告消失,反复测试,程序不再崩溃,非常稳定。看来问题产生的原因是因为漏包含了头文件,那么为什么不包含也可以编译通过呢?
五、一个简单的试验
/**********a.c**********/
#include <stdio.h>
int main()
{
void *p = func();
printf(“p=%lx\n”, p);
return 0;
}
/**********b.c**********/
void *func()
{
void *p = (void*)0x1234567890ABCDEF;
return p;
}
gcc –c a.c b.c,系统提示:
虽然有警告,但是编译却成功了。
我们再来链接一下,gcc –o all a.o b.o,链接也成功了。
运行程序,./all,输出结果是p=FFFFFFFF90ABCDEF,而我们期待的值是1234567890ABCDEF,问题重现了。
加上-Wall选项后重新编译,发现系统多了一行输出“警告:隐式声明函数 ‘func’”,
查阅了隐式声明的相关资料得知,原来,编译器会将所有隐式声明的函数的返回值类型都认定为int。
如此一来原因就比较清楚了,在b.c里定义的func函数返回的确实是指针类型,而在a.c里认为func返回的是int类型,程序运行时会将func返回的指针类型值强制转换为int,然后再强制转换为void*,赋给变量p。在64位机上,指针为8字节,int为4字节,在由指针转换为int时,高4字节被丢弃,值由0x1234567890ABCDEF变为0x90ABCDEF,然后在由int转换为指针的过程中,根据有符号数的补齐原则,按照int最高位是0还是1,将高4字节每一位全部补全为0或1。0x90ABCDEF的最高位是1,所以高4字节每一位都补全为1,最终形成了结果0xFFFFFFFF90ABCDEF。
那为什么实际运行时不是每次都崩溃呢?这是因为被调用的函数所返回的指针是动态分配的,其值事先不固定,如果被初始化的指针地址的高4字节和低4字节的最高位原本就是0,如0×0000000012345678,那么在将强制转换为int时丢弃高4字节对其就没有任何影响了,值还是0×12345678,然后再由int转为指针,高4字节补0,值为0×0000000012345678,所以程序可以正常运行下去。
六、编译与链接、定义与声明
在编译阶段,各个源文件独立编译,所以a.c和b.c是分开编译的,a.c里调用了func
函数而没有包含其声明(声明一般使用#include “b.h”,也可以使用extern函数原型的形式),编译器会认为func函数为隐式声明,将其返回值类型定为int。所以编译虽然有警告,但却成功了。
编译阶段是不需要函数的定义的。我们把b.c里的func函数注释掉,gcc –c a.c b.c一样可以执行成功。
在链接阶段,链接器将所有源文件编译得到的二进制文件以及调用的库链接到一个可执行文件中,此时链接器会去找func函数的具体定义,以供main函数调用。因为func函数确实有定义,所以链接也会成功。
链接阶段必须有函数的定义,否则链接器会报错。我们还是注释掉func,编译后再执行gcc –o all a.o b.o,系统输出如下:
在运行阶段,因为a.c在编译时认为func返回int类型,所以func的返回值(8字节指针)被截断为4字节的int,然后再进行高4字节的扩展,最后赋给了main函数里的变量p。在这两次类型转换中,p的值就有可能与func的返回值不相同了,p实际上已经成为一个野指针。
我们换用g++来编译看下效果:
看来g++对语法要求更严格,不允许隐式声明func函数。
返回值有可能因为隐式声明而不符合我们的期望,那么函数的参数呢?我们再来尝试一下。
/**********c.c**********/
extern void func(long);
int main()
{
func(0x1234567890ABCDEF);
return 0;
}
/**********d.c**********/
#include <stdio.h>
void func(int a)
{
printf(“a=%x\n”, a);
}
gcc -c c.c d.c -Wall,成功。
gcc -o all c.o d.o -Wall,成功。
运行程序./all,输出a=90abcdef,这显然不是我们想要的结果,但在编译和链接时却没有任何错误或警告报出。
使用g++编译,无法通过。
如果我们新建一个头文件d.h,将func函数的原型在d.h里声明,然后在c.c和d.c里都包含d.h,就可以避免参数或返回值可能的不一致了。
七、总结
- 在开发过程中,应该严格遵循先声明后定义、先声明后使用的原则,一方面保持良好的编码风格,另一方面也能避免很多潜在的错误;
- 从参数不一致造成的问题来看,最好不要使用extern声明函数,而应该使用包含头文件的形式;
- 编译时打开-Wall选项,对于编译过程中输出的每个WARNING都要仔细检查,防止出现各种匪夷所思的bug;
- 在某些场合,使用g++代替gcc可以获得更好的安全性。
小心C语言的定义与声明的更多相关文章
- C语言指针与数组的定义与声明易错分析
部分摘自<C语言深度解剖> 1.定义为数组,声明为指针 在文件1中定义: char a[100]; 在文件2中声明: extern char *a; //这样是错误的 这里的extern告 ...
- C语言全局变量的定义与声明
C语言中全局变量的定义与声明困扰着许多C语言初学者.本文讲述了全局变量定义与声明的用法,而且本为也将阐述这种用法的内在原理.我们先从两个错误例子引入,以下两个例程都在vc6.0平台上测试. 两种错误例 ...
- 详解keil采用C语言模块化编程时全局变量、结构体的定义、声明以及头文件包含的处理方法
一.关于全局变量的定义.声明.引用: (只要是在.h文件中定义的变量,然后在main.c中包含该.h文件,那么定义的变量就可以在main函数中作为全局变量使用) 方法1: 在某个c文件里定义全局变量后 ...
- keil采用C语言模块化编程时全局变量、结构体的定义、声明以及头文件包含的处理方法
以前写单片机程序时总是把所用函数和变量都写在一个c文件里,后来遇到大点的项目,程序动则几千行,这种方式无疑会带来N多麻烦,相信大家都有所体验吧! 后来学会了在keil里进行模块化编程,即只把功能相同或 ...
- c语言函数定义、函数声明、函数调用以及extern跨文件的变量引用
1.如果没有定义,只有声明和调用:编译时会报连接错误.undefined reference to `func_in_a'2.如果没有声明,只有定义和调用:编译时一般会报警告,极少数情况下不会报警告. ...
- C语言,函数的声明与定义
函数声明与定义 变量: 在讲变量前,先讲一下变量的声明和定义这两个概念. 声明一个变量,意味着向编译器描述变量的类型,但不为变量分配存储空间. 定义一个变量,意味着在声明变量的同时还要为变量分配存储空 ...
- 【转】c语言中的定义和声明
1. 变量的定义.声明 变量的声明有两种情况: 一种是需要建立存储空间的.例如:int a.在声明的时候就已经建立了存储空间.这种声明是"定义性声明(defining declaratio ...
- 1029 C语言文法定义与C程序的推导过程
1 阅读并理解提供给大家的C语言文法文件. 2 参考该文件写出一个自己好理解版的现实版的完整版的C语言文法. 3 给出一段C程序,写出用上述文法产生这段C程序的推导过程. program → exte ...
- 定义与声明、头文件与extern总结
用#include可以包含其他头文件中变量.函数的声明,为什么还要extern关键字? 如果我想引用一个全局变量或函数a,我只要直接在源文件中包含#include<xxx.h> (xxx ...
随机推荐
- Java将Unix时间戳转换成指定格式日期
public String TimeStamp2Date(String timestampString, String formats){ Long timestamp = Long.pars ...
- SQL server 2016 安装步骤
1.进入安装中心:可以参考硬件和软件要求.可以看到一些说明文档 2.选择全新安装模式继续安装 3.输入产品秘钥:这里使用演示秘钥进行 4.在协议中,点击同意,并点击下一步按钮,继续安装 5.进入全局规 ...
- vb 随机获取6个1-33的数
Private Sub random(ByVal num As Integer, ByVal min As Integer, ByVal max As Integer) Dim i As Intege ...
- EXTJS 表单提交
EXTJS框架中,在提交表单时,可有3种方式: 方法一: 普通的 Form 提交方式, 主要使用 form.submit() 方法来将表单提交到后台,在后台是根据表单的 name 属性来获取表单中元素 ...
- 学习记录 java泛型资料
java泛型资料: 1. 概述在引入范型之前,Java类型分为原始类型.复杂类型,其中复杂类型分为数组和类.引入范型后,一个复杂类型就可以在细分成更多的类型.例如原先的类型List,现在在细分成Lis ...
- spring HibernateValidator 验证 子类不起作用
spring HibernateValidator 验证 子类不起作用,在要验证的子类前加上@Valid即可. public class UserInfo { private int Id; @Val ...
- .NET的三种缓存(页面缓存,控件缓存,自定义缓存)
BLL.Area bll = new BLL.Area(); protected void Page_Load(object sender, EventArgs e) { if (Cache[&quo ...
- docker1.12 安装redis第三方集群方案 codis
docker1.12 安装redis第三方集群方案 codis
- 在CentOS 7上安装Python3.5源码包
最近开始系统学习Python 3.5,发现CentOS 7系统自带的python版本是Python 2.7.现在要使用Python 3.5该怎么办?方法大体跟安装其他程序一样.以下为详细经过: 1.事 ...
- 入门学习PHP之变量_1
1.函数里只能访问局部变量,不能访问全局变量,如果函数里需要访问全局变量则需要在变量前加global作用域,如下实例: <?php $x=5; $y=10; function myTest() ...