为什么这个C程序编译没有错误?

use*_*950 5 c compilation

我有两个C文件,main.c并且weird.c:

// main.c    
int weird(int *);

int
main(void)
{
    int x, *y;

    y = (int *)7;
    x = weird(y);
    printf("x = %d\n", x);
    return (0);
}

// weird.c

char *weird = "weird";
Run Code Online (Sandbox Code Playgroud)

但是,当我运行以下内容时:

clang -Wall -Wextra -c main.c
clang -Wall -Wextra -c weird.c
clang -o program main.o weird.o
Run Code Online (Sandbox Code Playgroud)

我没有得到任何错误.为什么是这样?不应该至少有链接错误?请注意,我只是在谈论编译文件 - 而不是运行它们.运行会产生分段错误.

Jon*_*ler 7

是否应该有链接器错误?

简短的回答"不应该至少存在链接错误吗?" 是"无法保证会出现链接错误".C标准没有强制要求.

正如Raymond Chen评论中指出:

语言律师的答案是该标准不需要诊断此错误.实际的答案是C没有用外部链接来装饰符号,因此类型不匹配未被检测到.

C++具有类型安全链接的原因之一是避免与此类似的代码问题(尽管主要原因是允许函数名称重载 - 解决此类问题可能更多是副作用).

C标准说:

§6.9外部定义

5 外部定义是一个外部声明,它也是函数(内联定义除外)或对象的定义.如果在表达式中使用通过外部链接声明的标识符(除了作为结果为整数常量的运算符sizeof_Alignof操作符的操作数的一部分),则整个程序中的某个地方应该只有一个标识符的外部定义; 否则,不得超过一个.

§5.1.1.1程序结构

1AC程序不需要同时翻译.该程序的文本保存在本国际标准中称为源文件(或预处理文件)的单元中.源文件以及通过预处理指令包含的所有头文件和源文件#include称为预处理转换单元.在预处理之后,预处理翻译单元被称为翻译单元.以前翻译的翻译单元可以单独保存或者保存在库中.程序的单独翻译单元通过(例如)调用其标识符具有外部链接的函数,标识符具有外部链接的对象的操纵或数据文件的操纵来进行通信.翻译单元可以单独翻译,然后链接以产生可执行程序.

5.1.1.2翻译阶段

  1. 解析所有外部对象和函数引用.链接库组件以满足对当前转换中未定义的函数和对象的外部引用.所有这样的翻译器输出被收集到程序映像中,该程序映像包含在其执行环境中执行所需的信息.

链接是基于外部定义的名称完成的,而不是基于名称标识的对象类型.程序员有责任确保每个外部定义的函数或对象的类型与其使用方式一致.


避免这个问题

评论中,我说:

这个[问题]是使用标题来确保程序的不同部分是连贯的论据.如果您从未在源文件中声明外部函数但仅在头文件中声明,并且在weird使用或定义相关符号(在本例中)的任何地方使用标题,则代码将不会全部编译.你可以有一个函数或一个字符串,但不能两者都有.你有一个标题weird.h包含两种extern char *weird;extern int weird(int *p);(但不能同时),都main.cweird.c将包括头,其中只有一个会成功编译.

到那边传来回应:

我可以添加到这些文件中以确保在main.c编译时检测到错误并抛出错误?

您将创建3个源文件.此处显示的代码比您通常使用的代码稍微复杂一些,因为它允许您使用条件编译来编译带有函数或变量的代码作为"外部链接的外部标识符" weird.通常,您可以选择一个预期的表示形式,weird并且仅允许它暴露.

weird.h

#ifndef WEIRD_H_INCLUDED
#define WEIRD_H_INCLUDED

#ifdef USE_WEIRD_STRING
extern const char *weird;
#else
extern int weird(int *p);
#endif

#endif /* WEIRD_H_INCLUDED */
Run Code Online (Sandbox Code Playgroud)

main.c

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

int main(void)
{
    int x, *y;

    y = (int *)7;
    x = weird(y);
    printf("x = %d\n", x);
    return (0);
}
Run Code Online (Sandbox Code Playgroud)

weird.c

#include "weird.h"

#ifdef USE_WEIRD_STRING
const char *weird = "weird";
#else
int weird(int *p)
{
    if (p == 0)
        return 42;
    else
        return 99;
}
#endif
Run Code Online (Sandbox Code Playgroud)

有效的编译序列

gcc -c weird.c
gcc -c main.c
gcc -o program weird.o main.o

gcc -o program -DUSE_WEIRD_FUNCTION main.c weird.c
Run Code Online (Sandbox Code Playgroud)

这两个都有效,因为代码被编译为使用该weird()函数.在两种情况下,标题都确保编译是一致的.

编译顺序无效

gcc -c -DUSE_WEIRD_STRING weird.c
gcc -c main.c
gcc -o program weird.o main.o
Run Code Online (Sandbox Code Playgroud)

这基本上与问题中的设置相同.weird.c编译该文件以创建一个名为的字符串weird,但main.c编译的代码希望使用一个函数weird().链接器确实链接了代码,但是当函数调用main()重定向到时,事情会发生灾难性的错误"weird".可能存储它的内存不可执行,因此执行失败.否则,该字符串被解释为机器代码,它可能没有做任何有意义的事情并导致崩溃.两者都不可取; 既不保证 - 这是调用未定义行为的结果.

如果你试图编译main.c使用-DUSE_WEIRD_STRING,编译将失败,因为头会指示weirdchar *,代码会试图使用它作为一个功能.

如果您weird.c使用字符串或函数(无条件)替换条件代码,则:

  • 如果文件包含函数但是-DUSE_WEIRD_STRING在命令行上设置,则编译将失败,
  • 或者,如果文件包含字符串但未设置,则编译将失败-DUSE_WEIRD_STRING.

通常,标题将包含一个无条件声明weird,作为函数或指针(但在编译时没有任何在它们之间进行选择的规定).

关键点是标头包​​含在两个源文件中,因此除非条件编译标志有所不同,否则编译器可以检查源文件中的代码是否与标头一致,因此两个目标文件有可能一起工作.如果通过设置编译标志来破坏检查,以便两个源文件在标题中看到不同的声明,那么你将回到原点1.

因此,标头声明接口,并检查源文件以确保它们遵循接口.标题是将系统保持在一起的粘合剂.因此,必须在其源文件之外访问的任何函数(或变量)应该在头文件中声明(仅一个头文件),并且该头文件应该在定义函数(或变量)的源文件中使用,并且在每个引用函数(或变量)的源文件中.你不应该写extern … weird …;一个源文件; 这样的声明属于标题.所有未在源文件外引用的函数(或变量)都应定义为static.这为您提供了在运行程序之前发现问题的最大机会.

您可以使用GCC来帮助您.对于函数,您可以在static引用或定义(非)函数之前坚持原型在范围内(并且在static引用static函数之前- 您可以在没有单独原型的情况下引用函数之前简单地定义函数).我用:

gcc -O3 -g -std=c11 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes \
    -Wold-style-definition -Wold-style-declaration …
Run Code Online (Sandbox Code Playgroud)

-Wall-Wextra暗示一些,但不是全部的,其他的-W…选项,所以这不是一个最小集.并非所有版本的GCC都支持这两种-Wold-style-…选择.但是,这些选项一起确保函数在使用函数之前具有完整的原型声明.


小智 0

当你想构建一个可执行程序时,你必须链接对象。

但现在,您只需编译源代码即可。

编译器想“啊,你稍后会编译很奇怪的.c。好吧。我就编译这个”