本博客可能随时删除或隐藏,请关注微信公众号,获取永久内容。
微信搜索:编程笔记本。获取更多干货。
今天我们一起来看一下 extern
函数时的几个细节问题。
首先,我们看一下示例代码的目录结构:
├── demo.c
└── my_print.c
再分别看一下源代码:
#include <stdint.h>
extern void my_print(uint8_t num);
int main(void)
{
my_print(254);
my_print(255);
my_print(256);
my_print(257);
return 0;
}
/* demo.c */
#include <stdio.h>
#include <stdint.h>
void my_print(uint16_t num)
{
printf("num=%u\n", num);
}
示例代码的大致意思是:在 main 函数中使用 extern
的方式调用其他文件中定义的函数。
细心的小伙伴可能注意到了,extern 的函数与其真正的原型不太一样:函数原型里的参数类型是 uint16_t
,而 extern
中的的参数类型是 uint8_t
。
如果我这样编译这两个 c 文件,能顺利编译通过吗?先停留一分钟,小伙伴们自己思考一下~
好了,我们来实际操作一下吧!
~/demo$ gcc demo.c my_print.c -o demo_c
demo.c: In function ‘main’:
demo.c:9:11: warning: unsigned conversion from ‘int’ to ‘uint8_t’ {aka ‘unsigned char’} changes value from ‘256’ to ‘0’ [-Woverflow]
9 | my_print(256);
| ^~~
demo.c:10:11: warning: unsigned conversion from ‘int’ to ‘uint8_t’ {aka ‘unsigned char’} changes value from ‘257’ to ‘1’ [-Woverflow]
10 | my_print(257);
| ^~~
~/demo$ ./demo_c
num=254
num=255
num=0
num=1
本博客可能随时删除或隐藏,请关注微信公众号,获取永久内容。
微信搜索:编程笔记本。获取更多干货。
可以看到,是能编译通过并成功运行的,只是会有一些 warning 而已。
看到这,有一个问题需要明确,难道 extern
时函数原型都可以不一致的吗?带着这个问题,我做了如下的实验。
使用 nm
工具查看可执行文件 demo_c
中的符号:
~/demo$ nm demo_c
0000000000004030 B __bss_start
0000000000004030 b completed.0
w __cxa_finalize@GLIBC_2.2.5
0000000000004020 D __data_start
0000000000004020 W data_start
0000000000001080 t deregister_tm_clones
00000000000010f0 t __do_global_dtors_aux
0000000000003df0 d __do_global_dtors_aux_fini_array_entry
0000000000004028 D __dso_handle
0000000000003df8 d _DYNAMIC
0000000000004030 D _edata
0000000000004038 B _end
00000000000011f4 T _fini
0000000000001130 t frame_dummy
0000000000003de8 d __frame_dummy_init_array_entry
0000000000002174 r __FRAME_END__
0000000000004000 d _GLOBAL_OFFSET_TABLE_
w __gmon_start__
000000000000200c r __GNU_EH_FRAME_HDR
0000000000001000 t _init
0000000000003df0 d __init_array_end
0000000000003de8 d __init_array_start
0000000000002000 R _IO_stdin_used
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
00000000000011f0 T __libc_csu_fini
0000000000001190 T __libc_csu_init
U __libc_start_main@GLIBC_2.2.5
0000000000001135 T main
0000000000001168 T my_print
U printf@GLIBC_2.2.5
00000000000010b0 t register_tm_clones
0000000000001050 T _start
0000000000004030 D __TMC_END__
文件中的符号非常多,但我们只关心这两个:
0000000000001135 T main
0000000000001168 T my_print
一个是主函数 main
,另一个是我们自定义的打印函数 my_print
。
现在,有细心的小伙伴可能发现问题了:难道 my_print
函数都不包含类型信息的吗?
如果真是这样的话,编译器也不知道这个函数的真正原型是什么了,只能按照我们 extern
时指定的类型去检查了。
- 答案是:C 函数名确实不带有参数类型信息。
这就能解释得通上面得问题了。在 demo.c
中,编译器认为函数 my_print
的参数类型是 uint8_t
,所以针对参数为 256
和 257
的调用都给出了警告,最终也相应地打印出 0
和 1
。
C++ 语言是支持多态的,一种方式就是:函数名相同,函数参数不同。要支持这种特性,就必须对函数名做一些修饰,一种方式就是将参数类型和函数名关联起来。
我们来看一下示例:
/* demo.cpp */
#include <cstdint>
extern void my_print(uint8_t num);
int main(void)
{
my_print(254);
my_print(255);
my_print(256);
my_print(257);
return 0;
}
此时,我们就编译不过了:
~/demo$ g++ demo.cpp my_print.c -o demo_cpp
demo.cpp: In function ‘int main()’:
demo.cpp:9:11: warning: unsigned conversion from ‘int’ to ‘uint8_t’ {aka ‘unsigned char’} changes value from ‘256’ to ‘0’ [-Woverflow]
9 | my_print(256);
| ^~~
demo.cpp:10:11: warning: unsigned conversion from ‘int’ to ‘uint8_t’ {aka ‘unsigned char’} changes value from ‘257’ to ‘1’ [-Woverflow]
10 | my_print(257);
| ^~~
/usr/bin/ld: /tmp/ccJn6UpE.o: in function `main':
demo.cpp:(.text+0xa): undefined reference to `my_print(unsigned char)'
/usr/bin/ld: demo.cpp:(.text+0x14): undefined reference to `my_print(unsigned char)'
/usr/bin/ld: demo.cpp:(.text+0x1e): undefined reference to `my_print(unsigned char)'
/usr/bin/ld: demo.cpp:(.text+0x28): undefined reference to `my_print(unsigned char)'
会提示我们函数未定义:
demo.cpp:(.text+0xa): undefined reference to `my_print(unsigned char)'
如果我们将 demo.cpp
文件中函数的外部声明改为:
extern void my_print(uint16_t num);
那么程序就能顺利编译通过并正确执行了:
~/demo$ g++ demo.cpp my_print.c -o demo_cpp
~/demo$ ./demo_cpp
num=254
num=255
num=256
num=257
同样地,我们使用 nm
看一下符号:
~/demo$ nm demo_cpp
0000000000004030 B __bss_start
0000000000004030 b completed.0
w __cxa_finalize@GLIBC_2.2.5
0000000000004020 D __data_start
0000000000004020 W data_start
0000000000001080 t deregister_tm_clones
00000000000010f0 t __do_global_dtors_aux
0000000000003df0 d __do_global_dtors_aux_fini_array_entry
0000000000004028 D __dso_handle
0000000000003df8 d _DYNAMIC
0000000000004030 D _edata
0000000000004038 B _end
00000000000011f4 T _fini
0000000000001130 t frame_dummy
0000000000003de8 d __frame_dummy_init_array_entry
0000000000002174 r __FRAME_END__
0000000000004000 d _GLOBAL_OFFSET_TABLE_
w __gmon_start__
000000000000200c r __GNU_EH_FRAME_HDR
0000000000001000 t _init
0000000000003df0 d __init_array_end
0000000000003de8 d __init_array_start
0000000000002000 R _IO_stdin_used
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
00000000000011f0 T __libc_csu_fini
0000000000001190 T __libc_csu_init
U __libc_start_main@GLIBC_2.2.5
0000000000001135 T main
U printf@GLIBC_2.2.5
00000000000010b0 t register_tm_clones
0000000000001050 T _start
0000000000004030 D __TMC_END__
0000000000001168 T _Z8my_printt
可以看到,my_print
被修饰成了 _Z8my_printt
。我们使用 c++filt
工具去还原一下函数名:
~/demo$ c++filt _Z8my_printt
my_print(unsigned short)
可以看到,与原函数声明一直。
结束语
在 C 语言中,一个函数的标识符就是其函数名,与其返回值、参数类型信息均无关。在 extern
函数时要确保正确性,否则编译器是无法检查出错误的。
本博客可能随时删除或隐藏,请关注微信公众号,获取永久内容。
微信搜索:编程笔记本。获取更多干货。
本博客可能随时删除或隐藏,请关注微信公众号,获取永久内容。
微信搜索:编程笔记本。获取更多干货。
本博客可能随时删除或隐藏,请关注微信公众号,获取永久内容。
微信搜索:编程笔记本。获取更多干货。