我该如何在一个 .cpp 文件中调用另一个 .cpp 文件中的函数?

16

我试着查找了一下,使用头文件之类的得到了一些混合的结果。

基本上,我有多个.cpp文件,其中包含我为二叉树、BST、链表等编写的全部函数。

与其每次复制和粘贴需要的函数,我想只需执行:

#include <myBSTFunctions.h>

我希望能够调用并使用自己编写的函数。

要实现这个目标需要哪些步骤呢?需要创建一个包含我使用的所有函数原型的头文件吗?

把定义了真正函数的 .cpp 文件和头文件放在什么地方才最合适呢?

有没有一种方法能够直接调用函数文件所在文件夹的路径呢?也就是说,我更想把它放在与主源代码的 .cpp 文件相同的文件夹中,以便与我的同事分享。

我该如何做到这一点呢?

这是在 Windows 操作系统下使用 MinGW 编译器的方法。


3
平台?工具链? - pm100
5
有用的阅读材料:编译/链接过程是如何工作的?。不同的编译器工具链之间,实现方式可能有所不同。对于 g++,它可以像这样简单:g++ A.cpp B.cpp - user4581301
3
阅读一本有关C++的书可能有助于理解多文件程序:这里有几本不错的书 - Carl
4
编译器如何知道在哪里找到a.cpp?如果它不在当前工作目录中,你必须告诉编译器它在哪里。g++ A.cpp ../../source/b/B.cpp的意思是在当前文件夹中查找A.cpp,并向后返回两个文件夹,在source/b中查找B.cpp。请注意,这只是告诉编译器文件的位置,并不会改变文件本身。 - user4581301
通常情况下,cpp文件中声明的所有函数默认具有外部链接性,也就是说,在一个文件中定义的函数可以通过前向声明在另一个cpp文件中使用。(因此,通过#include .h文件,您可以获得要调用的位于另一个cpp文件中的函数的前向声明。)然而,使用static关键字在函数声明之前将其链接性限制为内部,也就是说,静态函数不能在其定义之外的文件中使用。 - undefined
显示剩余2条评论
5个回答

27

在头文件中声明函数:

// File MyFunctions.h
int myFunction1(int, int);
int myFunction2(int, int);

在文件MyFunctions.cpp中实现它们:

// File MyFunctions.cpp
#include "MyFunctions.h"
int myFunction1(int a, int b){
    // Your code

int myFunction2(int a, int b){
    // Your code
}
在任何你想要的文件中包含标题:
// OtherFile.cpp
#include "MyFunctions.h"
// Now you have access to the functions defined in MyFunctions.h in this file

我不了解MinGW,但它应该看起来像这样:

g++ otherfile.cpp MyFunctions.cpp...

谢谢!我终于开始意识到为什么/如何工作了(我想)。感觉.h文件类似于函数原型。像这样使用它们可以使其清晰明了,但到目前为止,我只看到它们与类一起使用,所以它总是看起来像“类”这个神奇的黑盒子中的另一个部分。 - Ac Hybl
"miniGW" 明显是 MinGW 的拼写错误。 - Peter Mortensen

18
您似乎在构建程序方面缺少一些非常基本的概念。我将为您提供一个非常基本的入门指南,但是您需要在其他地方找到更完整的答案,以真正理解正在发生的事情并获取特定于您设置的详细信息。
1. 您通常会告诉编译器编译每个cpp文件。当cpp文件具有#include语句时,这基本上会在编译之前将include的文件复制并粘贴到cpp文件中(这是由预处理器完成的)。编译器处理的每个完整单元(带有包括)称为翻译单元。每个翻译单元生成一个目标文件。
2. 目标文件包含已编译的代码,但它们通常不是完整的。也就是说,它们包含对未包含在其中的代码的引用。基本上,它们可以说“现在调用此函数,我不知道它在哪里或它做什么,但是你应该调用它”。
3. 然后使用链接器将目标文件与库一起链接成可执行文件(或库)。链接器的工作是通过在其他目标文件和库中找到相关代码来“解析”每个目标文件中的所有对外部代码的引用。
4. 库有两种类型:共享库(Windows中的.dll)和静态库。静态库通过链接器链接到可执行文件(或其他库)中,这意味着您可以在没有库的情况下使用可执行文件(相关库代码成为可执行文件的一部分)。您还可以将可执行文件/库与共享库链接在一起,在这种情况下,每次运行可执行文件都需要一个该共享库的副本-操作系统将需要在运行之前将编译的代码动态链接到共享库中。
所以,回到您的问题。
您大致有三个选择:每次在单个项目中直接编译并链接所有cpp文件;将有用的可重复使用代码编译为静态库,然后将您的项目与其链接;将有用的可重复使用代码编译为共享库,将您的项目与其链接,并确保随结果一起发布共享库以便运行。
任何合理大小的项目通常都会结合至少两个选项。多个cpp文件将成为项目代码的一部分,将作为单独的翻译单元进行编译,并提供给链接器。大多数项目还将使用某些库(无论是您自己编写的还是其他人编写的),根据需要静态或动态链接。
不幸的是(在我看来),C ++作为一种语言并没有一个单一的构建系统来组织所有这些(更近期的语言通常是这样)。有几个不同的编译器/链接器和许多不同的构建系统都可以执行所有这些操作。需要采取的具体步骤非常取决于您选择的编译器和构建系统。

8
那很简单。正如JMAA所说,你应该做些研究来理解这些概念,但实际上,这是你要做的:

你需要定义一个functionsExample.cpp文件,在其中定义所有函数,并创建一个functionsExample.h文件,在其中声明你的函数。

你将在functionsExample.cpp文件中包含以下内容:

#include "functionsExample.h"
#include <iostream>
int example(int x, int y)
{
    return x + y;
}

请将此内容翻译为中文:
将其作为 functionsExample.h 实现:
#ifndef FUNCTIONSEXAMPLE_H
#define FUNCTIONSEXAMPLE_H

int example(int x, int y);

#endif

然后在.cpp文件中,您想运行示例函数。只需添加:

#include "functionsExample.h"

但是如我说的那样,您应该对头文件保护、预处理器指令和文件组织进行一些研究,以深入了解这个问题。以下是一些我推荐的链接:

头文件

预处理器指令


1
最好的做法是在functionsExample.cpp中也包含#include "functionsExample.h",正如第一个链接建议的那样:“这样可以让编译器在编译时而不是链接时捕获某些类型的错误。” - starriet

1

最简单的方法是使用构建工具。我个人偏好Meson,但CMake 更受欢迎。还有其他的,但这两个都不会错。它们都是跨平台的,支持不同的工具链。这意味着只要你的代码是标准的C++,你就可以在MinGWVisual StudioG++Clang上编译它,而不需要更改任何内容。它们让你从平台上可用的工具链中选择。每个工具都在其网站上有一个快速入门教程。

配置文件由您编写,指定要使用哪些程序源文件以及要构建哪些可执行文件。您在配置步骤中选择使用的构建链,然后可以通过运行MakeNinja(Meson所需且建议使用CMake)来构建可执行文件。如果更改源代码,只需要重新运行Make或Ninja。当然,只有更改部分才会被重新构建。受到更改影响的可执行文件也将被重新链接。

观看一些详细的构建过程可能会有教育意义,以便熟悉您系统上的构建过程。

PS:传统上,在#include中,您使用尖括号(<>)表示系统头文件,使用引号("")表示自己的头文件。这与首先查找它们的位置有关。


0

这篇文章旨在为现有答案提供补充,因为它们没有解决一个非常普遍的问题(特别是对于初学者):如何在单独的源文件中包含带有模板的头文件。


基本上我有多个.cpp文件,其中包含了我为二叉树、BST、链表等所编写的所有函数。
由于:
- 您已经标记了您的问题 , - C++ 拥有模板技术, - 您拥有类似容器的数据结构(BST、链表等),我假设这些数据结构使用了模板,
我想指出其他答案中没有提到的一个常见问题。请考虑以下两个文件:
文件 class.hpp
#pragma once // Non-standard but supported by almost (if not) all compilers

// Some class
template<class T> class Class {
    T data;
public:
    Class();
};

// Some function
template<class T> T square(const T& x);

文件 class.cpp

#include "class.hpp"
#include <iostream>

template<class T> Class<T>::Class()
{
    std::cout << "Class()" << '\n';
}

template<class T> T square(const T& x)
{
    return x*x;
}

尝试实例化一个Class对象,或调用square()函数:
#include "class.hpp"
#include <iostream>

int main()
{
    Class<int> clss; // should output Class()
    std::cout << square<int>(3); // should print 9
}

使用以下命令进行编译:

g++ class.cpp main.cpp -o cpptest.exe

./gcc/bin/ld.exe: main.cpp(.text+0x15): undefined reference to 'Class<int>::Class()'
./gcc/bin/ld.exe: main.cpp(.text+0x28): undefined reference to 'int square<int>(int const&)'

问题

1. 模板

假设你有这个函数:

template<class T> T add(const T& x, const T& y)
{
    return x + y;
}

你可以这样调用:

int res = add<int>(2, 3);

在调用时刻(即编译器看到对add()的调用时),编译器会创建函数实现的副本:

int add(const int& x, const int& y)
{
    return x + y;
}

然后你的调用将变成:

int res = add(2, 3);

这显然意味着当调用被找到时,模板函数的实现必须对编译器可见,以便编译器能够复制它。而为了让编译器找到/看到它,它必须在同一个翻译单元中。

这就是模板魔法在幕后的工作原理。有关更多信息,请参阅此答案

2. 编译器和链接器

通过编译步骤:

  1. 将所有的#include指令替换为所包含文件的内容。
  2. 单独编译每个.cpp文件(也称为“翻译单元”)。结果将是每个单元的目标文件。请注意,一些代码(如main.cpp中的代码)引用另一个代码(class.cpp)。这将在下一步中解决。
  3. 将翻译单元链接在一起形成可执行文件。(有关详细信息,请参见JMAA答案)。

应用上述第1步,我们将有两个翻译单元(.cpp文件):

文件class.cpp

template<class T> class Class {
    T data;
public:
    Class();
};

template<class T> T square(const T& x);

/* <iostream> code ... */

template<class T> Class<T>::Class()
{
    std::cout << "Class()" << '\n';
}

template<class T> T square(const T& x)
{
    return x*x;
}

文件 main.cpp

template<class T> class Class {
    T data;
public:
    Class();
};

template<class T> T square(const T& x);

/* <iostream> code ... */

int main()
{
    Class<int> clss;
    std::cout << square<int>(3);
}

应用以上步骤2,我们将只有地址(这不是真正的对象代码的外观,这只是一个简化):

文件 class.cpp:

/* Nothing to generate for 'Class' definition. Remember, templates are just models (hence the word "template"), not concrete classes/functions. */

/* Nothing to generate for square()'s definition: It's a template, not concrete. */

iostream_object_code

/* Nothing to generate for 'Class' implementation. It's a template, not concrete. */

/* Nothing to generate for square()'s implementation: It's a template, not concrete. */

文件 main.cpp:

/* Class is not concrete: nothing is generated. */

/* square() is not concrete: nothing is generated. */

iostream_object_code

int main()
{
    class_int_obj_placeholder clss;
    __operator_xx(cout_obj_ref, fn100017(3));
}

执行上述步骤3,您将得到undefined reference错误:链接器不知道任何翻译单元中的fn100017()。因为square()是一个模板(或者模型),编译器没有生成任何具体实现。对于Class也是同样的情况。

解决方案

  1. 显而易见的解决方法是将定义和实现都放在同一个头文件中。这样,在main.cpp中包含时,两者都将对编译器可见,因为它们在同一个翻译单元中。
  2. 或者,如果您想保持定义和实现分离,可以在头文件中进行实现,然后在定义头文件的末尾包含它:

文件class.hpp

#pragma once

template<class T> class Class {
    T data;
public:
    Class();
};

template<class T> T square(const T& x);

#include "class_impl.hpp"

文件 class_impl.hpp

#include <iostream>

template<class T> Class<T>::Class()
{
    std::cout << "Class()" << '\n';
}

template<class T> T square(const T& x)
{
    return x*x;
}

使用相同的命令编译并运行main.cpp

Class()
9

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接