Bootstrap

CC++ 标头和源文件:它们如何工作?

本文将向您展示将程序划分为C中的组件部分或正确使用标头和源文件C++诀窍。

介绍

我主要是为我的一个朋友写这篇文章的。但是,如果我不与大家分享这一点,我会对社区造成伤害,所以就在这里。

我们将探索标头和源文件以及它们的作用。这些代码的大部分在 C 和 C++ 中都有效,但C++特定的代码将被标识为这样。

我会保持简短,并涵盖要点。

了解这个烂摊子

标头和源文件之间的关系起初可能会令人困惑。在几年前自学C和C++时,我努力理解这些关系以及在哪里使用它们。

部分原因是没有完全理解原型设计的必要性,也没有理解C或C++中的链接过程。我们将在这里讨论这个问题。

原型

在使用结构、变量或函数之前,必须声明它。对于函数,您可以并且通常应该在函数实现本身之前为函数提供原型。原型本质上只是函数签名,即返回类型、名称、参数和任何修饰符,如 or (C++)。staticconst

请考虑以下事项:

C++

int sum(int lhs, int rhs);

这是名为 的函数的函数原型。请注意,我们没有在它之后放置它,也没有在其中进行任何实现。相反,它以分号结尾,向编译器指示这是一个原型而不是函数本身。sum{}

声明原型后,您可以使用该函数,即使它稍后出现在文件中,甚至在单独的 C 或 C++ 源文件中。

没有标头

当您用于包含头文件时,编译器(技术上是预处理器)实际上将包含的内容复制到包含它的文件中,即指令出现的行。这发生在实际编译任何源代码之前。这在以后发生。#include#include

因此,编译过程本身没有标头。它们都已转换为C++源文件,标头直接复制到源文件本身中。

如果我们把原型放在头文件(mymath.h)中,把实现放在源文件(main.c)中,这一切都有效:sum()

我的数学

C++

#ifndef MYMATH_H#define MYMATH_Hint sum(int lhs, int rhs);

#endif // MYMATH_H

你可能会问 // 的东西是怎么回事?由于 C 和 C++的工作方式,您很可能会经常多次包含标头,因为您将包含多个文件,而这些文件本身包含相同的文件。周围的内容确保编译器只处理第一个包含。#ifdef#define#endifsum()

在C++中,首选的替代方案是:

C++

#pragma onceint sum(int lhs, int rhs);

您应该始终在编写的任何标头中使用这些技术之一。

主.c

C++

#include <stdio.h>#include "mymath.h"int main(int argc, char** argv) {

    printf("2 + 3 = %d\n", sum(2, 3));

}

int sum(int lhs, int rhs) {

    return lhs + rhs;

}

您会注意到第一个包含周围的尖括号。这意味着编译器将搜索预定义的包含文件夹。这通常意味着“系统”和“标准”标头。使用引号,预处理器相对于源文件的目录进行搜索。

在 中,我们没有首先在此文件中声明它。通常,我们必须至少提供一个原型(或完整的实现)才能使用该功能。但是,我们有 - 只是不是在这个文件中,而是在mymath.h中。main()sum()

在编译器开始编译之前,让我们或多或少地查看此代码的最终形式。

main.c (预处理)

C++

// <stdio.h> omitted but would otherwise be here//ifndef MYMATH_H//define MYMATH_Hint sum(int lhs, int rhs);

//endif // MYMATH_Hint main(int argc, char** argv) {

    printf("2 + 3 = %d\n", sum(2, 3));

}

int sum(int lhs, int rhs) {

    return lhs + rhs;

}

这就是编译器“看到”的内容(减去注释,我只是在上面添加了注释以明确所有内容。

在此演绎版中,原型在使用函数之前就已存在,这满足了编译器的要求。

但是为什么?

特别精明的读者可能想知道为什么我们会同时拥有头文件和源文件的麻烦,而我们可以将所有内容都放在头文件中,并且只使用一个包含所有头的文件的源文件。

您很快就会发现这不起作用,特别是如果您有多个包含相同标头的源文件。由于重复的函数实现,你将收到链接器错误。

通常,您希望将实现保留在源文件中,将 or (C++) 声明和函数原型保留在标头中,并将这些内容的实现保留在关联的源文件中。可以创建“仅标头”库,它们既有一些优点,也有一些缺点。创建仅标头库超出了此处的范围。structclass

C++中有一些例外,例如模板。当您声明 时,声明原型和实现是不现实的,因此没有C++标准。因此,所有模板代码 - 包括实现都属于标头。C++中的另一种情况是你有一个函数。您可以将该实现放在标头中,但不必这样做。templateinline

多个源文件

编译器为项目中的每个源文件创建一个二进制文件。然后,它获取这些二进制文件并将它们链接在一起,从而将这些二进制文件合并到单个可执行文件中。

多个源文件允许您更好地组织源代码,并且还可以将来自第三方的源代码包含在项目中更加现实。

正如 steveb 在本文的评论中指出的那样,它还允许C++编译器仅重新编译它需要的源文件,因此如果一个更改,它不需要重新编译所有其他文件。

但是,如前所述,您必须设计仅包含原型和类型声明的头文件,否则您将无法在多个源文件中使用该标头,因为该函数将存在重复的实现,即使它们位于不同的二进制文件中。一旦链接器尝试将二进制文件链接在一起,它就会失败,因为它找到了函数的多个副本,并且不知道要使用哪个副本。

一般来说,当您创建头文件时,它应该具有具有相同基本名称但不同扩展名的关联源文件。例如,我们可能有一个名为mymath.c的源文件来赞美mymath.h

mymath.c

C++

#include "mymath.h"int sum(int lhs, int rhs) {

    return lhs + rhs;

}

请注意,我们包含了关联的标头。这并不总是绝对必要的,但通常是绝对必要的,这是很好的做法,因为它可以帮助读者了解哪个头文件属于它。

现在在main.c中,我们需要删除实现。编译代码时,将指定两个源文件。根据工具链,可以将多个源文件传递给为您调用链接器的编译器,或者编译每个源文件并作为单独的步骤自行运行链接器。sum()

结论

希望这能澄清 C 和 C++ 头文件和源文件背后的一些谜团。有了这些知识,您应该能够更好地组织您的项目,并了解其他项目。祝您编码愉快!

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;