5

难以捉摸的C语言,这个程序为什么会这样执行呢?

 3 years ago
source link: https://blog.popkx.com/%E9%9A%BE%E4%BB%A5%E6%8D%89%E6%91%B8%E7%9A%84c%E8%AF%AD%E8%A8%80-%E8%BF%99%E4%B8%AA%E7%A8%8B%E5%BA%8F%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BC%9A%E8%BF%99%E6%A0%B7%E6%89%A7%E8%A1%8C%E5%91%A2/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

难以捉摸的C语言,这个程序为什么会这样执行呢?

发表于 2019-08-29 15:08:55   |   已被 访问: 321 次   |   分类于:   C语言   |   暂无评论

昨天浏览外文论坛帖子,发现一个有趣的问题。这个问题产生的原因是程序员编写C语言代码不规范造成的,这也是很多初学者容易犯的错——只关注核心功能,而不关注细节。问题是这样的:

小明在源文件 f1.c 里定义了一个布尔函数,相关的C语言代码是下面这样的:

#include <stdbool.h>

bool f1()
{
    int var1 = 1000;
    int var2 = 2000;
    int var3 = var1 + var2;
    return (var3 == 0) ? true : false;
}
d92567e45e628072cc109455b73261b2.png

显然,函数 f1() 执行后,变量 var3 的值等于 3000,因此必定会返回 false。但是小明在 main.c 文件里编写 main() 函数调用 f1() 后,发现结果似乎有些奇怪,main() 函数的C语言代码如下:
#include <stdio.h>
#include <stdbool.h>

int main()
{
    printf( f1() == true ? "true\n" : "false\n");
    if( f1() )
    {
        printf("executed\n");
    }
    return 0;
}

按理说,既然 f1() 函数总是返回 false,上述 main() 函数被编译执行后,应该只会输出“false”才对,但是小明得到的实际执行结果却是下面这样的:

$ gcc main.c f1.c -o test
$ ./test
false
executed

这是怎么回事呢?小明使用的编译器版本为 gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2。

仔细观察小明的编译命令gcc main.c f1.c -o test,应该能够发现他并没有指定任何别的编译选项,因此 gcc 编译器默认实现的是 1990 年(很老了)的 C90 标准。

C90 标准在处理C语言代码时,有一个主要缺点就是:如果某段C语言代码调用函数时,程序员未能实现指定被调用函数原型,那么将使用默认的函数原型,即默认被调用函数的原型为:

int func();

也即默认被调用函数可以接收任意多的参数,并且返回值类型为 int。这样的默认规则在处理函数返回值时可能会有一个转换过程(当被调用函数原型返回值不是 int 类型时),但是它并不会修改实际的函数实现。

现在小明遇到的问题就清楚了:f1() 函数原本的返回值类型为 bool 型,但是他在编写 main() 函数时,并未事先指定 f1() 函数的原型,因此编译器默认将其当作是 int 返回值类型了。

而 bool 型和 int 型的 size 并不一致,所以小明编写的C语言程序行为就属于“未定义”的了。出现什么样的结果都是不足为奇的、

能够看出,C90 标准在遇到未知函数原型时,会默认将其当做 int func(); 原型的特性其实是一种危险的特性,因此,从 C99 标准开始,这样的特性就被禁止了。

但是不幸的是,知道 5.x.x 版本的 gcc 默认属性仍然是较老的 C90 标准,这可能是为了兼容之前的C语言代码。要解决小明遇到的问题也是简单的,只需要告诉 gcc 编译器希望使用的标准就可以了。例如:

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11 用于告诉编译器遵循 C11 标准。
  • -pedantic-errors 告诉编译器全心全意的编译C语言代码,一旦发现错误就给出错误提示。
  • -Wall 可以尽可能的让编译器发现一些不规范的代码,并给出相应的警告提示。
  • -Wextra 类似于 -Wall 选项,它能够让编译器发现更多不规范的代码。

使用上述命令编译小明的C语言代码,应该会发现此时编译器不再能够完成编译了,而是给出了错误提示:“implicit declaration of function ‘f1’”:

049ed8f2226c69e13b026b94b4b08bb7.png

此时,编译器不再“猜测”f1() 函数的原型,而是强制我们在调用函数前指定其原型。因此对小明的C语言代码稍作修改,如下:
2be56c5b7156183a2a1e7bc001f512d7.png

此时再编译执行,发现一起符合预期了:
# gcc -std=c11 -pedantic-errors -Wall -Wextra main.c f1.c -o test
# ./test 
false

很多C语言初学者觉得细节不重要,重要的是核心算法或者代码,这样的心态其实很危险,很容易写出难以捉摸的程序,本节就是一个例子。事实上,越是初学者就越应该严格遵守规范,这样才能尽可能的避免出现奇怪的,难以捉摸的结果,打击自己的学习信心。

阅读更多:   C语言


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK