当包含一个头文件时会发生什么?

8

我有两个疑问:

  1. 头文件实际上包含什么?所有函数定义还是只有原型声明?
  2. 当我包含一个头文件时会发生什么?头文件的所有内容都会附加到我的代码中吗?还是特定(或全部)头文件的内容被加载到内存中,根据我的代码调用函数?

3
它是逐字复制在您编写的#include行的确切位置。 - pablo1977
1
“头文件实际上包含什么?”-- 试着看一下。 - Jim Balter
@JimBalter:你有什么建议可以找到并查看它吗?这并不是显而易见的做法。 - user2357112
网上有数百万个C头文件,只需点击几下即可获得,大多数语言教材中也有。谷歌搜索“示例C头文件”会带来大量信息。如果你希望成为一名程序员,我敦促你发展基本基础的智力技能。在SO上提问并不能弥补他们的缺失。 - Jim Balter
这是从上述谷歌搜索中得到的一个有用结果:http://web.stanford.edu/~fringer/teaching/operating_systems_03/handouts/lecture8.pdf - Jim Balter
4个回答

12

C和C++是编程语言,它们广泛使用名为函数转发的特性。这意味着您可以这样说:

void f(int i); /* note the semicolon */

这意味着:"我保证,在源代码的某个地方,将会有人定义函数f实际上是做什么的"。在这样的前置声明之后,您可以使用此函数,就好像它真实存在一样,并且编译器将要求稍后实际定义此函数(如果没有定义,则无法编译)。这种前置声明也被称为头文件,因为它实际上就是函数定义的头部。
void f(int i) /* Header */
/* Body */
{   
    /* ... */
}

头文件是一个文件,主要包含这些前置声明。您可以使用头文件来访问在其他地方定义的函数(例如在不同的编译单元或外部库中),然后附加所需的对象文件或库以提供这些头文件的实现。除了函数前向声明之外,在头文件中还可以找到结构定义、常量或其他项目,这些项目对于正确使用定义的函数是必需的。

编译器如何将您的前向声明与.c文件中的实际实现匹配呢? 很简单 - 通过头文件。它尝试查找与您先前声明的头文件完全匹配的函数定义(实现)。

如果您#include头文件会发生什么?编译器(特别是预处理器)将整个头文件的内容复制到您放置#include的位置。那就是所有的魔法,没有更多的事情发生。

在运行时,头文件根本不重要,因为您的可执行文件仅由可执行代码组成。编译器加载库中的所有可用函数(由头文件访问)或(大多数情况下,如果启用了优化)仅选择您实际在代码中使用的这些函数。

有趣的是,编译器只在有人实际使用该功能时才需要函数定义(实现)。否则,该前向声明将被忽略。尝试:

void f(int i);

int main(int argc, char ** argv)
{
    /* Do not use f here */

    return 0;
}

“头文件是一个文件,主要包含这样的前置声明。”-- 不对。许多甚至大多数的头文件除了函数原型外,还包含结构体、类型定义、宏、常量等内容。 - Jim Balter
在运行时,头文件并不重要,因为你的可执行文件仅由可执行代码组成。头文件可以包含可执行代码,特别是内联函数。说头文件在运行时无关紧要就像说源文件在运行时无关紧要...从技术上讲是正确的,但毫无意义。 - Jim Balter
从这个问题中,我得到了一个印象(也许是错误的),即OP认为头文件在运行时被加载到内存中——因此我的回答。 - Spook
除了函数转发之外,在头文件中您还可以找到结构定义、常量或其他项目,这些项目对于定义的函数的正确使用是必需的。当我再仔细想一想时,仅由常量和结构组成的头文件并不是很有用,除非您提供一个可以使用它们的函数。然而,宏则是另一回事。我写C++,但从我的经验来看,大多数头文件主要包含函数/类,以及作为补充的结构、常量、宏等。 - Spook
@JimBalter 最初,OP在函数的角度上问了关于头文件的问题,所以我在这个上下文中编写了答案。我的意思是,为了在一个头文件中使用所有这些结构体,您无论如何都必须包含其他带有函数的头文件(至少这是答案中那部分的重点)。 - Spook
显示剩余2条评论

7

头文件到底包含什么?所有函数定义还是只有原型声明?

头文件包含函数声明、外部变量、宏、结构等。最佳实践是将函数定义放在.c文件中。

当我包含一个头文件时会发生什么?头文件的所有内容都会附加到我的代码中吗?还是说特定(或全部)头文件的内容被加载到内存中,根据我的代码调用函数?

头文件不过是将其内容插入使用#include的地方。如果愿意,您可以自己编写所有内容。

包含头文件等同于复制头文件的内容,但我们不这样做,因为这很容易出错,而且在源文件中复制头文件的内容不是一个好主意,特别是如果我们有多个源文件组成程序。

编辑:

有关程序完整执行的内存布局,您可以查看此链接http://fgiasson.com/articles/memorylayout.txt


好的。但是.c文件在哪里?编译器如何知道并使用该函数?函数定义如何被加载到内存中以供执行? - user2684198
1
“那么.c文件在哪里呢?”-- 它会在你的硬盘上某个地方。“编译器怎么知道呢?”-- 因为你告诉它源文件的名称。“并使用该函数”-- 编译器不使用函数,它们生成调用函数的代码。“函数定义如何被带入内存以供执行?”-- 函数定义不会被带入内存也不会被执行......代码会被带入内存,包括编译器生成的实现函数的所有代码。 - Jim Balter

2

头文件应该包含有用的函数声明(以及可能的变量声明),类型定义(如structunionenumtypedef)和宏定义(函数式或对象式宏)。这些通常声明了某个库的一部分提供的功能。例如,标准头文件(如<stdio.h>)描述了标准库的一部分。现代头文件可能包含inline函数定义,但除此之外,头文件不应定义任何函数或变量,只应声明它们。
另外,公共头文件通常只声明对源代码使用者有用的类型和函数。它只应包括其他提供头文件所需声明的头文件;通常不应包括只用于实现头文件所描述的功能的额外头文件。
头文件及其包含的任何头文件的内容将被复制到翻译单元(TU)中。注意#define宏和条件处理(如#if等)会被记录下来。通常可以找到一种方法来查看给定源文件的预处理器输出,以查看C编译器实际看到的内容(但要小心,它可能非常冗长)。例如,选项通常是-E(由POSIX命令c99要求)或-P(一个较旧的常用选项)。
TU是编译器实际看到的内容。它包含了你文件中的源代码以及你所包含的头文件中的相关信息。

(1)应包括宏和常量。 - Jim Balter

0

只是一点小提醒,可能很明显......

  • 广告2/ 如果您#include一个文件,则包含文件中的#include指令将被所包含的文件替换。 这就是发生的事情。 这是预处理器执行的操作。 根据定义

  • 广告1/ 头文件可以继续任何内容(编译器可以接受的任何内容)。 其他答案描述了用于在c和h文件之间组织代码的常规约定,但这些仅是约定。 如果不遵循它们,您很可能会在更大的项目中迷失方向。

实际上,如果您为非常简单的微控制器编写c代码,则完全可以没有头文件及其包含。

如果您是学生/爱好者/有时间,则运行预处理器并查看中间输出可能很有用。 有趣的事实:很久以前我曾使用c预处理器生成过定制信件。

无论如何,我的观点是:c预处理器的作用由标准定义,而文件之间的代码组织是一种您/您的团队在一定程度上可以选择的约定。这种区别非常重要。


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