【问题标题】:Calling a function from another file in the same directory in C从C中同一目录中的另一个文件调用函数
【发布时间】:2011-09-30 22:15:05
【问题描述】:

我正在学习 C,但我在 Java 等高级编程语言方面拥有长期经验。

我正在阅读有关头文件的信息,所以我在玩它们,但是我注意到我可以从另一个文件调用一个函数而不#include 它(它在同一个目录中),这怎么可能?! 是这样配置的make文件,链接器还是什么?

我们有两个文件

main.c
add.c

main.c 从 add add.c 调用函数add(int x,int y),但我在#include add.c 之前错误地编译了它,它工作了!更令人困惑的是,当我#include add.c 时,它在函数add 上给出了多定义错误

【问题讨论】:

  • 不要包含 .c 文件。 即使您成功构建,包含 .c 文件也会让其他人更难构建您的程序,因为他们必须分清哪些 .c 文件是翻译单元,哪些是标题。

标签: c gcc header linker


【解决方案1】:

这里发生了一些不同的事情。首先,我将介绍多个文件的基本编译是如何工作的。

如果你有多个文件,重要的是函数的声明和定义之间的区别。定义可能是你在定义函数时习惯的:你把函数的内容写出来,比如

int square(int i) {
    return i*i;
}

另一方面,声明让你向编译器声明你知道一个函数存在,但你不告诉编译器它是什么。例如,你可以写

int square(int i);

编译器会期望函数“square”在别处定义。

现在,如果您想要互操作两个不同的文件(例如,假设函数“square”在 add.c 中定义,并且您想在 main.c 中调用 square(10)),您需要同时定义和声明。首先,在 add.c 中定义 square。然后,您在 main.c 的开头声明它。这让编译器在编译 main.c 时知道有一个函数“square”在别处定义。现在,您需要将 main.c 和 add.c 编译到目标文件中。你可以通过调用来做到这一点

gcc -c main.c
gcc -c add.c

这将生成文件 main.o 和 add.o。它们包含已编译的函数,但不是完全可执行的。这里要理解的重要一点是 main.o 在某种意义上是“不完整的”。在编译 main.o 时,您告诉它函数“square”存在,但函数“square”没有在 main.o 中定义。因此 main.o 对函数“square”有一种“悬空引用”。除非您将它与另一个包含“square”定义的 .o(或 .so 或 .a)文件结合起来,否则它不会编译成完整的程序。如果您只是尝试将 main.o 链接到一个程序中,即

gcc -o executable main.o

你会得到一个错误,因为编译器会尝试解决对函数“square”的悬空引用,但不会找到它的任何定义。但是,如果在链接时包含 add.o(链接是在将 .o 文件转换为可执行文件或 .so 文件时解析所有这些对未定义函数的引用的过程),则不会有任何问题。即

gcc -o executable main.o add.o

这就是功能上在 C 文件中使用函数的方法,但是风格上,我刚刚向您展示的是“不是正确的方式”。我这样做的唯一原因是因为我认为它会更好地帮助您了解正在发生的事情,而不是依赖“#include magic”。现在,您可能已经注意到,如果您必须在 main.c 的顶部重新声明要使用的每个函数,事情会变得有些混乱.标头的想法是它包含函数的声明,没有它们的定义。这样,为了使用 add.c 中定义的函数编译程序,您无需手动声明正在使用的每个函数,也无需在代码中 #include 整个 add.c 文件。相反,您可以#include add.h,它只包含add.c 的所有函数的声明

现在,复习一下#include:#include 只是将一个文件的内容直接复制到另一个文件中。所以,例如,代码

abc
#include "wtf.txt"
def

完全等价于

abc
hello world
def

假设 wtf.txt 包含文本“hello world”。

所以,如果我们把 add.c 的所有声明都放在 add.h 中(即

int square(int i);

然后在main.c的顶部,我们写

#include "add.h"

这与我们刚刚在 main.c 顶部手动声明函数“square”在功能上相同。

所以使用标头的一般想法是,您可以拥有一个特殊的文件,该文件只需#include 即可自动声明您需要的所有功能。

但是,标头还有一种更常见的用途。假设 main.c 使用来自 50 个不同文件的函数。 main.c 的顶部看起来像:

#include "add.h"
#include "divide.h"
#include "multiply.h"
#include "eat-pie.h"
...

相反,人们经常将所有这些#include 移到 main.h 头文件中,而只将 #include main.h 从 main.c 移到。在这种情况下,头文件有两个的用途。它声明 main.c 中的函数以供其他文件包含时使用,并且它包含 main.c 的所有依赖项,当包含在 main.c 中时。以这种方式使用它还允许 chains 依赖项。如果你#include add.h,你不仅会得到add.c中定义的函数,而且你也隐式得到add.c使用的任何函数,以及他们使用的任何函数,等等.

另外,更巧妙的是,#include 来自它自己的 .c 文件的头文件会隐式检查您所犯的错误。例如,如果您不小心将 square 定义为

double square(int i);

在 add.h 中,您通常可能不会意识到,直到您链接 main.o 正在寻找 一个 square 的定义,而 add.o 正在提供 另一个,不兼容 em> 一。这将导致您在链接时遇到错误,因此直到构建过程的后期您才会意识到错误。但是,如果您将 add.c 中的 #include add.h 添加到编译器,您的文件看起来像

#include "add.h"
int square(int i) {
    return i*i;
}

处理#include 语句后的样子

double square(int i);
int square(int i) {
    return i*i;
}

编译器在编译 add.c 时会注意到什么,并告诉你。有效地,以这种方式包含您自己的标题可以防止您向其他文件错误地宣传您提供的功能类型。

为什么不用声明就可以使用函数

正如您所注意到的,在某些情况下,您实际上可以使用一个函数,而无需每次都声明它或#include 任何声明它的文件。这是愚蠢的,每个人都同意这是愚蠢的。然而,这是 C 编程语言(和 C 编译器)的一个遗留特性,如果你使用一个函数而不首先声明它,它只是假设它是一个返回类型“int”的函数。因此,实际上,使用函数隐含地将该函数声明为一个函数,如果它尚未声明,则返回“int”。如果您考虑一下,这是非常奇怪的行为,如果您这样做,编译器应该会警告您。

标题保护

另一种常见做法是使用“Header Guards”。为了解释标头守卫,让我们看一个可能的问题。假设我们有两个文件:herp.c 和derp.c,它们想使用彼此包含的函数。按照上述指南,您可能有一个带有该行的 herp.h

#include "derp.h"

还有一个带有行的derp.h

#include "herp.h"

现在,如果你想一想,#include "derp.h" 将被转换为derp.h 的内容,而derp.h 又包含#include "herp.h" 行,这将被转换为内容herp.h 和 that contains... 等等,所以编译器将永远继续下去,只是扩展包含。类似地,如果 main.h #包括 herp.h 和 derp.h,并且 herp.h 和 derp.h 都包括 add.h,我们看到在 main.h 中,我们以 two结尾> add.h 的副本,一份是#include herp.h 的结果,一份是包含derp.h 的结果。那么,解决方案呢? “标头守卫”,即防止任何标头被#included 两次的一段代码。例如,对于 add.h,正常的做法是:

#ifndef ADD_H
#define ADD_H

int sqrt(int i);
...
#endif

这段代码本质上是告诉预处理器(编译器中处理所有“#XXX”语句的部分)检查“ADD_H”是否已经定义。如果不是 (ifndef),那么它首先定义“ADD_H”(在这种情况下,不必将 ADD_H 定义为 as 任何东西,它只是一个已定义或未定义的布尔值),然后定义标题的其余内容。但是,如果已经定义了 ADD_H,那么 #include 这个文件将执行 nothing,因为在 #ifndef 块之外没有任何内容。所以这个想法是,只有第一次包含在任何给定文件中时,它才会实际向该文件添加任何文本。之后,#include 它不会向您的文件添加任何其他文本。 ADD_H 只是您选择的任意符号,用于跟踪是否已包含 add.h。对于每个标题,您使用不同的符号来跟踪它是否已被包含。例如,herp.h 可能会使用 HERP_H 而不是 ADD_H。使用“header guard”将解决我上面列出的任何问题,其中包含文件的重复副本,或者#includes 的无限循环。

【讨论】:

  • 您的意思是倒数第二个代码块中的 #include "add.h" 而不是 #include "add.c" - 我会更正它,但 SO 将不再接受仅几个字符的编辑。
  • 如果您添加一点关于保护包含文件免受多个包含的内容,这将很容易成为这里的最佳答案。
  • @Software Monkey:好主意。会的。
  • 非常感谢您的详细回答:) 但是对于标题守卫,如果#include 完全作为在包含文件中复制包含的文件。那么下面应该给出一个错误,但它不会;而且我没有任何包含守卫:我们有 5 个文件 A.c ,B.c,B.h ,C.c,C.h B.h 包括 C.h A.c 包括 B.h 和 C.h(再次)这不应该引发多重定义错误吗?因为 A.c 最终将有 2 个 C.h 副本(一个直接,另一个通过 B.h)?编译器没有抱怨,它运行完美,没有守卫,这怎么可能?
  • @MemoryLeaks:有两个副本是对的,但两个副本并不总是错误,有时只是效率低下。根据标题的内容,如果它有两个副本,它仍然是一个有效的程序。例如,我相信如果你在同一个文件中定义了一个函数或变量两次并且它们是相同的,那没关系(不要引用我的话)。当然,我希望您的编译器会警告您重新定义 Ch 中的所有内容(您可以尝试选项 -Wall all 警告。)因此,如果没有标头保护,它可能会起作用,但仍然效率低下+不好
【解决方案2】:

问题是您不应该#includeing .c 文件。

要在另一个文件中使用一个函数,你需要声明它。通常,每个 .c 文件(main.c 除外)都有一个关联的头 (.h) 文件,该文件正确地声明了 .c 文件中定义的所有函数。您可以声明任意多次(只要所有声明都相同),但只能有一个定义

当您#include "add.c" 时发生的情况是 add.c 的文本包含在 main.c 中,给 main.ca 定义(以及,作为副作用,声明) add。然后,当您自己编译 add.c 时,它会创建add另一个 定义。因此,函数有两个定义,编译器会因为不知道使用哪一个而吓坏了。

如果你把它改成#include "add.h",add.h 看起来像这样:

#ifndef ADD_H
#define ADD_H

extern int add(int x, int y);

#endif /* ADD_H - Google "include guard" for more info about this trickery */

那么main.c有一个add的声明,可以使用该函数,但是add定义相当牢固地只存在于add.c文件中,所以只存在一次,所以它会正确编译。

【讨论】:

    【解决方案3】:

    这是一个从不同c程序调用函数的简单示例

    让我将主程序命名为 main.c 并将保存函数的程序命名为 function.c 的 function.c 我正在创建名为 function.h 的头文件

    main.c

    #include"function.h"
    int main()
    {
         int a = sum(1,2);
         return a;
    }
    

    函数.c

    int function(int a,int b)
    {
        return a+b;
    }
    

    函数.h

    int function(int,int);
    

    要编译使用下面给出的命令

    g++ main.c function.c -o main

    这里有详细的解释。在主程序中,我调用了对 2 个数字求和的函数。 主程序中的值 1 和 2 通过标头 function.h 提供给 function.c 中的函数,其中包含访问点或到 function.c 的桥

    更多详情,您可以查看下面给出的链接

    http://www.cplusplus.com/forum/beginner/34691/

    https://social.msdn.microsoft.com/Forums/en-US/4ea70f43-a0d5-43f8-8e24-78e90f208110/calling-a-function-in-a-file-from-another-file?forum =winembplatdev

    添加打印语句以检查结果或使用 echo $?文件 main 执行后

    【讨论】:

    • 如果需要检查结果添加打印语句
    【解决方案4】:

    您可以调用它,因为在 C 中调用时不需要声明。但是返回类型未知,因此默认为 int。这可能部分是由于 C 中的默认调用约定,以及类型的默认提升到至少 int 精度。

    如果您包含定义您正在调用的函数的标头,编译器能够检查对该函数的调用是否具有正确数量和类型的参数。

    如果您包含函数定义,除非您使用static 指定它们的存储,否则它们将被导出。由于您还在编译和链接add.c,因此您无法添加此内容,因为您的两个或两个目标文件都不会导出add

    如果您想简单地包含所有函数,最好将它们的定义放入标题中,并在它们上面撒上存储说明符。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-08-18
      • 2021-10-13
      • 1970-01-01
      • 2011-06-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-04-25
      相关资源
      最近更新 更多