可怕的extern关键字一、不利之处二、例子三、分析四、正确做法

实际项目中看到有人用extern关键字来声明外部函数,这是一个很不好的行为。

一、不利之处

如果函数原型改变的话,每个extern声明的地方都要改一遍。 如果有地方没改到呢? 我们通过一个例子来看下悲剧是怎么发生的。

二、例子

头文件api.h中声明了一个函数func:

#ifndef __API_H__
#define __API_H__

void func(int a);

#endif

文件api.c中实现了func函数:

#include <stdio.h>
void func(int a)
{
    printf("hello world.[%d]\n", a);
}

文件bad_test.c中调用了func函数,但是func被重新声明成无参数的了

#include <stdio.h>
extern void func();
int main(int argc,char *argv[])
{
    func();
    return 0;
}

编译运行结果如下:

$  gcc -Wall bad_test.c api.c
$ ./a.out 
hello world.[1]
$ 

三、分析

  1. 编译的时候即时加了-Wall选项也没有编译警告。 这是因为编译是以源文件为单位的,在bad_test.c中func的声明是无参的,调用也是按无参调用的,所以编译器不会告警。 如果把extern声明去掉,编译器好歹还能给个“函数未显式定义”的警告。
  2. 链接的时候也没报错? 这是因为,在C语言中,编译出来的函数符号表是不带参数的,如下所示, func在符号表中就是字符串func。 这也是为啥C语言不能做编译时多态的原因。 所以,别指望在链接的时候报错。 $ gcc -c api.c $ nm api.o 0000000000000000 T func U printf $
  3. 程序竟然还能运行??? 程序输出了1, 但是这个1是哪里来的呢? 我们先看看下面这一系列输入输出: $ ./a.out hello world.[1] $ ./a.out a hello world.[2] $ ./a.out a b hello world.[3] $ ./a.out a b c hello world.[4] $ ./a.out a b c d hello world.[5] 看明白了吗? 竟然把argc的值打了出来!!! 程序运行的时候,调用的肯定还是带参数的func函数,但是这个参数从哪里来呢? 考虑到默认从右到左的压栈顺序,处于栈顶的argc被取出来塞给func函数作为入参了,所以func打印出来的是argc的值。 都这样了,接下来离各种莫名其妙的异常还远吗? 这种问题定位起来会搞死人的。

四、正确做法

建议通过头文件引用的方式来使用外部函数,如果bad_test.c写成下面这样,编译就无法通过,可以有效阻止错误蔓延。

#include <stdio.h>
#include "api.h"

int main(int argc,char *argv[])
{
    func();
    return 0;
}

编译报错:

$ gcc bad_test.c api.c -c
bad_test.c: In function ‘main’:
bad_test.c:6:5: error: too few arguments to function ‘func’
     func();
     ^
In file included from bad_test.c:2:0:
api.h:3:6: note: declared here
 void func(int a);
      ^
$ 

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏前端侠2.0

co yield避免嵌套详细代码示例。

1391
来自专栏ShaoYL

iOS 声明属性关键字讲解

35517
来自专栏知无涯

XML创建或改变某个新属性

2808
来自专栏测试开发架构之路

堆和栈的区别

一、预备知识—程序的内存分配          一个由C/C++编译的程序占用的内存分为以下几个部分     1、栈区(stack)— 由编译器自动分配释放,存...

2868
来自专栏漫漫全栈路

ASP.NET MVC 行为详解

前面分别介绍了MVC中的三个重要部分,而行为,则是其中C-Controller中的重要内容,下面详解一二。 一般继承自Controller类,类Controll...

2804
来自专栏决胜机器学习

Shell基本操作与命令

Shell基本操作与命令 (原创内容,转载请注明来源,谢谢) 本文主要是我最近学习shell语言的学习笔记,主要在于通过学习这些内容,达到看得懂shell脚...

3485
来自专栏Ryan Miao

java并发编程读书笔记(1)-- 对象的共享

1. 一些原则 RIM(Remote Method Invocation):远程方法调用 Race Condition:竞态条件 Servlet要满足多个线程的...

3598
来自专栏博岩Java大讲堂

多线程--同步与锁

2263
来自专栏Java编程技术

什么是重排序与中断

Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序可以保证最终执行的结果是与程序顺序执行的结果一致,并且只会对不存在数据依赖性的指令进行重排序,...

822
来自专栏码字搬砖

JVM内存模型之运行时常量池

运行时常量池 jdk7之前属于方法区的一部分,jdk8之后属于Metaspace,在heap中。 运行时常量池俗称常量池,主要用于存放编译期生成的各种字...

2201

扫码关注云+社区

领取腾讯云代金券