【问题标题】:is the purpose of header files in C only warning to users?C 中的头文件的目的是只警告用户吗?
【发布时间】:2018-11-11 23:28:30
【问题描述】:

我是链接的初学者,如果我的问题太基本,请见谅。假设我有两个 .c 文件

file1.c 是

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

file2.c 是

int function2() 
{ 
    return 2018;
}

我知道规范是,创建一个 file2.h 并将其包含在 file1.c 中,我有一些问题:

第一季度。 #include in file1.c 对我来说并没有太大的区别或改进,我仍然可以在没有 file2.h 的情况下正确编译 file1.c,编译器只会警告我 'implicit declaration of function 'function2',但是这个警告有很大帮助吗?程序员可能知道 function2 是在其他 .c 文件中定义的(如果您使用 function2 但未定义它,您当然知道该定义在其他地方)并且链接器将完成其工作以生成最终的可执行文件?所以 include file2,c 对我来说的唯一目的是,在编译过程中不要显示任何警告,我的理解是否正确。

第二季度。想象一下这种情况,程序员在file1.c中定义了function2,他不知道他的function2与file2.c中的那个冲突,直到链接器抛出错误(显然他可以正确编译他的file1.c。但是如果我们希望他在编译他的file1.c时知道他的错误,添加file2.h仍然没有帮助,那么添加头文件的目的是什么?

第三季度。我们应该添加什么让程序员知道他应该为 function2 选择一个不同的名称,而不是在最后阶段被链接器告知错误。

【问题讨论】:

  • 假设函数是double function2(char *str) 现在你认为编译器将如何处理假设(或曾经假设)int 参数和返回值的“隐式”函数?头文件的一个目的是使函数原型可用。
  • 这样的话,编译器肯定会报错,但是即使程序员使用了正确的原型,如果他定义了自己的function2,那么链接器也会报错,我们该如何停止或者抛出编译的时候报错,可以在早期发现问题?
  • 让链接器通知您重复有什么问题?为什么您希望编译器在编译一个模块时查看所有其他 C 源文件?这似乎不会在每个编译/链接周期中发生,或者会节省大量时间。
  • @amjad the compiler will throw an error for sure 不,它会警告你,使用了隐式函数声明。
  • @KamilCuk 不行,编译器肯定会报错,你可以试试 gcc -s 看看

标签: c header-files compiler-warnings


【解决方案1】:

C89 3.3.2.2 Function calls强调我的:

如果函数调用中带括号的参数列表之前的表达式仅包含一个标识符,并且如果此标识符没有可见的声明,则该标识符是隐式就像在包含函数调用的最里面的块中声明一样,声明

    extern int  identifier();

出现

现在,请记住,空参数列表(在 () 大括号内不声明任何内容)声明了一个采用 unspecified 类型和参数数量的函数。在大括号内输入 void 以声明函数不接受任何参数,例如 int func(void)

第一季度:

这个警告有很大帮助吗?

是和不是。这是一个主观问题。它可以帮助那些使用它的人。作为个人说明,请始终将此警告视为错误。使用 gcc 编译器使用-Werror=implicit-function-declaration。但是你也可以忽略这个警告,制作最简单的main() { printf("hello world!\n"); } 程序。

链接器会完成它的工作来生成最终的可执行文件吗?所以 include file2,c 对我来说的唯一目的是,在编译过程中不显示任何警告,我的理解是否正确。

没有。如果使用不同/不兼容的指针类型调用函数。它调用未定义的行为。如果函数被声明为void (*function2(void))(int a);,那么调用((int(*)())function2)() 是UB,就像在没有先前声明的情况下调用function2()。根据附件 J.2(资料性):

在以下情况下行为未定义:

指针用于调用类型与指向的类型(6.3.2.3)不兼容的函数。

根据C11 6.3.2.3p8:

指向一种类型的函数的指针可以转换为指向另一种类型的函数的指针,然后再返回;结果应与原始指针比较。如果转换后的指针用于调用类型与引用类型不兼容的函数,则行为未定义。

所以在你的幸运情况下int function2() 确实有效。例如,它也适用于 atoi() 函数。但是调用atol() 会调用未定义的行为。

第二季度:

链接器抛出错误

这应该发生,但实际上是依赖于链接器的。如果您使用 gcc 编译器使用单个阶段编译所有源代码,则会引发错误。但是,如果您创建静态库,然后使用 gcc 编译器链接它们而不使用 -Wl,-whole-archive,那么它将选择第一个声明是 sees,请参阅 this thread

添加头文件的目的是什么?

我猜是简单和有序。这是在开发人员和库之间共享数据结构(枚举、结构、typedef)和声明(函数和变量类型)的一种方便且标准的方式。还可以共享预处理器指令。图片您正在编写一个包含 1000 多个文件的大型库,可以与 100 多个其他库一起使用。在每个文件的开头你会写struct mydata_s { int member1; int member2; ... }; int printf(const char*, ...); int scanf(const char *, ...); etc. 还是只写#include "mydata.h"#include <stdio.h>?如果您需要更改mydata_s 结构,则需要更改项目中的所有文件,并且所有其他使用您的库的开发人员也需要更改定义。我并不是说你不能这样做,但这样做会花费更多的工作,而且没有人会使用你的库。

第三季度:

我们应该添加什么让程序员知道他应该为 function2 选择一个不同的名称,而不是在最后阶段被链接器告知错误。

如果发生名称冲突,链接器会通知您(希望)它找到了两个具有相同名称的标识符。您需要创建一个工具来准确检查您的来源。我不知道为什么需要这样做,链接器是专门用来解析符号的,所以它自然会处理存在具有相同标识符的两个符号的情况。

【讨论】:

    【解决方案2】:

    简答:

    带走:编译器越早发出警报越好。

    Q1:.h 的含义:一致性和早期警报。及早提醒常见的错误方式可以提高代码的可靠性,并减少调试和生产崩溃。

    Q2:冲突名称为开发人员带来早期警报,通常更容易修复。

    Q3:早期重复定义警报未纳入 C 标准。

    练习: 1. 在一个文件中定义一个函数,该函数 printf("%d\n",i) 是一个 int 参数,然后在另一个文件中调用该函数,其浮点数为 42.0。 2. 用 (double)42.0 跟注。 3. 使用 %.s 下打印的 char *str 参数定义函数,然后使用 int 参数调用。

    更长的答案:

    流行约定:在典型使用中,.h 文件的名称源自 .c 文件或与之关联的文件。文件.h 和文件.c。对于具有许多定义的 .h 文件,比如 string.h,从其中的内容(如在 str... 函数中)的角度派生文件名。

    我的大原则:总是更好地构建代码结构,以便编译器可以在编译时立即警告错误,而不是让它们滑过调试或运行时,因为它们依赖于以正确方式运行的代码。运行时错误可能很难诊断,特别是如果它们在程序投入生产后很长时间就会出现,并且维护成本高昂并且会降低您的客户体验。见“尤达记法”。

    Q1:.h 的含义:一致性和早期警报以及提高代码的可靠性。

    C .h 文件允许在不同时间编译的 .c 文件的开发人员共享通用声明。没有重复的代码。 .h 文件还允许从所有文件中一致地调用函数,同时识别不正确的参数签名(参数计数、错误冲突等)。拥有定义函数的.c 文件还#include .h 文件有助于确保定义中的参数与调用一致;这听起来可能很简单,但如果没有它,签名冲突的所有人为错误都可以潜入。

    仅当所有调用者的参数签名与定义中的参数签名完全匹配时,省略 .h 文件才有效。通常情况并非如此,因此如果没有 .h 文件,任何冲突的签名都会产生错误的数字,除非您在调用文件中也有并行的外部变量(坏坏坏)。像 int vs float 这样的东西会产生非常错误的参数值。错误的指针会产生段错误和其他完全崩溃。

    优点:使用 .h 文件中的外部变量,编译器可以正确地将不匹配的参数转换为正确的类型,从而确保更好的调用。虽然你仍然可以搞砸论点,但可能性要小得多。它还有助于避免不匹配适用于一种实现而不适用于另一种实现的情况。

    隐式声明警告对我非常有帮助,因为它们通常表明我忘记了 .h 文件或将名称拼错为外部名称。

    Q2:名称冲突。早期警报。

    冲突的名称是不好的,避免问题是开发人员的责任。 C++ 解决了名称空间的问题,而 C 作为较低级别的语言则没有。

    使用 .h 文件可以让编译器诊断提醒开发人员注意冲突的早期阶段。如果编译器诊断不这样做,希望链接器会在多定义符号错误上这样做,但标准不保证这一点。

    伪造名称空间的一种常见方法是在 .h 中使用一些前缀(extern int filex_function1(int arg, char *string) 或 #define FILEX_DEF 42)开始所有可能发生冲突的定义。

    如果正在使用的两个不同的外部库共享相同的名称,该怎么办超出了此答案的范围。

    Q3:早期重复警报。抱歉……早期警报取决于实施。

    这对于 C 标准来说很难定义。由于 C 是一门古老的语言,因此有许多创造性的不同方式来编写和存储 C 程序。 在使用它们之前寻找冲突的名称取决于开发人员。交叉参考程序等工具可以提供帮助。甚至像与 vim 或 emacs 相关的 ctags 这样愚蠢的东西也可以提供帮助。

    【讨论】:

      【解决方案3】:

      你误解了头文件和函数原型的用法。

      1. 需要头文件来在多个代码文件之间共享公共信息。此类信息包括宏定义、数据类型,可能还有函数原型。

      2. 编译器需要函数原型才能正确处理返回数据类型,并就滥用函数返回类型和参数向您发出早期警告。

      3. 函数原型可以在头文件中声明,也可以在使用它们的文件中声明(更多类型)。

      您有一个非常简单的示例,只有 2 个文件。现在想象一个包含数百个文件和数千个函数的项目。您将迷失在链接器错误中。

      'c' 允许您使用由于遗留原因而未声明的函数。在这种情况下,它假定函数的返回类型为“int”。但是,现代数据类型比早期具有更大的真实性。该函数可以返回指针、64 位数据、结构。要表达你必须使用原型,否则什么都行不通。编译器必须知道如何正确处理函数返回。

      此外,它还可以就参数类型的错误使用向您发出警告。由于遗留问题,这些仍然是警告,但它们在早期的 c++ 中得到了解决并转换为错误。

      这些警告为您提供了早期调试功能。在某些情况下,类型不匹配警告可以为您节省几天的调试时间。

      因此,在您的示例中,您不需要头文件。您可以使用“extern”语法在“main”文件中对函数进行原型设计。你甚至可以不用原型设计。但是,在真正的现代编程世界中,您不能允许后者。特别是当您在团队中工作或希望您的程序可维护时。

      将函数原型存储在头文件中是个好主意。这将是一个很好的文档来源,特别是对于好的 cmets。顺便说一句,函数名称必须有意义才能可维护。

      【讨论】:

        【解决方案4】:

        第一季度。是的。 C 是一种低级语言,历史上用于将低级结构绑定到高级概念。例如,传统上标签 _end 位于程序中的最后一个地址。标签是无类型的,但您可以将其声明为您方便的任何类型。 “正确键入”的语言会使这种滥用变得困难。

        第二季度。按照惯例,file1.c 和 file2.c 都将包含 file2.h;一个是消费者,另一个是生产者。遵循这个简单的习语将捕获声明与定义错误;虽然同样,“警告”不一定是强制执行的。

        第三季度。许多软件组织采用“警告就是错误”的规则来对他们的程序员进行社会控制。

        【讨论】:

          猜你喜欢
          • 2021-09-04
          • 2019-02-12
          • 1970-01-01
          • 1970-01-01
          • 2013-01-03
          • 2012-01-20
          • 2015-07-17
          • 2020-12-29
          • 2018-06-14
          相关资源
          最近更新 更多