【C语言笔记】【陷阱系列】 函数声明问题
陷阱系列内容。用于记录各式各样有陷阱的C语言情况☺。
陷阱代码
我们看下如下代码:
首先是一个func.c
文件,申请一段内存用于存放字符串,将字符串内容拷贝进去,最后将内存地址返回。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* func(void)
{
printf("entry\n");
char *str = malloc(6);
strcpy(str, "hello");
printf("str %p\n", str);
printf("exit\n");
return str;
}
然后是一个func.h
文件,内容如下:
char* func(void);
最后是test.c
文件的main
函数,通过func
函数获取一个字符串,然后打印出来。注意这里没有包含func.h
文件。
#include <stdio.h>
//#include "func.h"
int main(int argc, char* argv[])
{
char *str = func();
printf("str %p\n", str);
printf("str %s\n", (char *)str);
return 0;
}
接下来编译一下上述代码:
$ gcc -o test test.c func.c -Wall
发现在没有包含func.h
的情况下,还可以正常编译通过,但是有警告信息。
运行一下这个程序:
$ ./test
entry
str 0x7fffc87bf2b0
exit
str 0xffffffffc87bf2b0
Segmentation fault (core dumped)
发现这个程序会出现 Segmentation fault 段错误。从打印信息有entry
和exit
来看,程序确实正常调用了func
函数,但是内存地址错了,在func
函数中,申请的地址是0x7fffc87bf2b0
,而返回的地址是0xffffffffc87bf2b0
。
下面我们将test.c
文件的中//#include "func.h"
的屏蔽去掉,如下:
#include <stdio.h>
#include "func.h"
int main(int argc, char* argv[])
{
char *str = func();
printf("str %p\n", str);
printf("str %s\n", (char *)str);
return 0;
}
重新编译运行:
$ gcc -o test test.c func.c -Wall
$ ./test
entry
str 0x7fffc29a52b0
exit
str 0x7fffc29a52b0
str hello
可以看到结果是对的,打印出了hello
,不会段错误了,申请的内存地址也都正确了。
为什么没有包含func.h
也能正常调用func
函数?但是调用的结果返回值会出错?
说明
重新编译一下没有包含func.h
的代码, 查看其的警告信息:
$ gcc -o test test.c func.c -Wall
test.c: In function ‘main’:
test.c:13:17: warning: implicit declaration of function ‘func’ [-Wimplicit-function-declaration]
13 | char *str = func();
| ^~~~
test.c:13:17: warning: initialization of ‘char *’ from ‘int’ makes pointer from integer without a cast [-Wint-conversion]
可以看到了两条警告,第一条警告表示隐式声明函数func
,另一条警告表示初始化时将整数赋给指针,未作类型转换,也就是初始化一个char *
类型时却赋值一个int
类型的值,没有使用强制类型转换。
上面的这些测试,我都是使用64位的平台,更换了一个32位平台后发现,也会出现如上的警告,但是运行不会出错,没有段错误,申请的内存地址也都一致了。
# ./test
entry
str 0x88a3008
exit
str 0x88a3008
str hello
所以造成上述问题的原因是什么呢?还得从编译警告入手。
第一条警告提示隐式声明函数func
,那么什么是隐式声明呢?在 C89 的 3.3.2.2 Function calls 中介绍如下:
If the expression that precedes the parenthesized argument list in a function call consists solely of an identifier, and if no declaration is visible for this identifier, the identifier is implicitly declared exactly as if, in the innermost block containing the function call, the declaration
extern int identifier();
appeared.
也就是没有声明的函数,自动使用隐式声明,创建一个声明,例如这个程序没有声明func
,就会自动声明一个func
如下:
extern int func();
可以看到,因为隐式声明的存在,所以链接时可以找到func
函数。但是隐式声明函数的返回值是一个int
类型的值,这也就是为什么会有第二条警告的原因,此时声明的func
函数的返回值是int
类型的值,所以会警告初始化一个char *
类型时却赋值一个int
类型的值。
由于使用的是 64 位的平台,所以指针的大小为 8 个字节,整型数的大小为 4 个字节,返回的指针地址只剩下了低 32 位。看之前的测试结果,返回的地址是0x7fffc87bf2b0
, 返回的是int
型,所以只剩下了低 32 位的0xc87bf2b0
,又由于赋值给了char *
,由于int
是有符号数,而0xc87bf2b0
的最高位为 1 ,所以类型提升时高位全部都会补1,也就变成了0xffffffffc87bf2b0
。
$ ./test
entry
str 0x7fffc87bf2b0
exit
str 0xffffffffc87bf2b0
Segmentation fault (core dumped)
因此得到的地址就是错误的,访问该地址会出错,导致段错误的出现。
上述也同时解释了为什么在 32 位平台上,编译同样会有警告,但是却不会段错误。因为在 32 位平台上,指针的大小和整型数的大小同为 4 个字节,类型转换后不会出现数据损失,因此没有出错。
值得注意的是,隐式声明在 C99 中已将其删除,在 C99 的 Foreword 的第 5 条中写了与上一版本相比的主要变化包括哪些。
This second edition cancels and replaces the first edition, ISO/IEC 9899:1990, as amended and corrected by ISO/IEC 9899/COR1:1994, ISO/IEC 9899/AMD1:1995, and ISO/IEC 9899/COR2:1996. Major changes from the previous edition include:
其中有一条如下:
remove implicit function declaration
删除了隐式函数声明。
然后我们在 C99 的 6.5.2.2 Function calls 章节中也就看不到隐式函数声明了。
If the function is defined with a type that does not include a prototype, and the types of the arguments after promotion are not compatible with those of the parameters after promotion, the behavior is undefined
也就是说目前的C标准中已经没有隐式声明函数了,但是 GCC 默认还允许隐式函数声明功能,只会发出一个警告,所以我们使用GCC编译就有警告而不是错误。
[参考资料]
Implicit function declarations sometimes work in C?
Implicit function declarations and linkage
本文链接:https://blog.csdn.net/u012028275/article/details/130939259