首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C编程必知:为什么每个头文件都需要#ifndef/#define

C编程必知:为什么每个头文件都需要#ifndef/#define

原创
作者头像
李述铜
修改2025-11-03 16:25:41
修改2025-11-03 16:25:41
1640
举报
文章被收录于专栏:C语言C语言

大家好,我是李述铜,一名专注于嵌入式系统与底层开发的工程师,我的主要工作是制作课程带大家从零手写操作系统、TCP/IP协议栈、文件系统等核心系统,从实现的视角理解计算机底层原理。

最近,我已经读完了《C Primer Plus》,也写了一些文章总结我自己比较感兴趣的内容。应一些同学的要求,接下来会写一些文章介绍同学们在使用C语言过程中,比较有意思的知识点或编程技巧。

这篇文章主要涉及C语言中的#ifndef和#define。


在C语言开发中,我们能在几乎每个头文件看到类似的代码,比如:

代码语言:javascript
复制
#ifndef __FOO_H__
#define __FOO_H__

// 头文件内容

#endif

相信不少同学都会有这样的困惑,例如:

“为什么要写#ifndef/#define? 如果不写,会有什么问题?

今天,这篇文章将会帮你彻底搞明白上述问题。


一、问题场景:如果不使用#ifndef/#define会怎样?

我们知道,C语言编译器在处理#include时,由其预处理器对该指令进行处理。预处理器的工作很简单:把文件内容直接插入到当前文件中的该位置

也就是说:#include = 文本替换,即将#include所在的行,用文件内容做替换。

这样做可以方便代码的编写,并实现代码的重用。但是,如果一个头文件被多次#include,就会出现重复定义等编译或链接错误

示例一:简单问题示例

这里举一个简单的例子:

代码语言:javascript
复制
#include "foo.h"
#include "foo.h"   // 重复

int main() {}

如果foo.h中定义了结构体类型:

代码语言:javascript
复制
struct Point{int x, y;};

重复include就会导致生成两个struct Point的定义,此时编译是会出错的(有的编译器可能视为警告):

代码语言:javascript
复制
struct Point{int x, y;};
struct Point{int x, y;};

int main() {}

示例二:复杂问题示例

现实中,极少有人像上面那样在同一个文件中连续#include同一文件,而更多的是直接或间接#include。例如:

假设有两个头文件foo.h和bar.h:

代码语言:javascript
复制
// foo.h
struct Point{int x, y;};
// bar.h
#include "foo.h"

然后在main.c中使用#include:

代码语言:javascript
复制
#include "foo.h"
#include "bar.h"   // 间接又包含了 foo.h

int main() {
    struct Point p;
    p.x = 1;
    p.y = 2;
    return 0;
}

在对main.c进行编译时,编程器会进行如下操作:

  1. #include "foo.h" → 复制foo.h内容 struct Point{int x, y;};
  2. #include "bar.h"→ 复制bar.h内容 #include "foo.h" 由于有#include "foo.h“,于是再复制foo.h内容 → 又生成一次struct Point;

最终展开后的代码类似:

代码语言:javascript
复制
struct Point{int x, y;};
struct Point{int x, y;};

int main() {
    struct Point p;
    p.x = 1;
    p.y = 2;
    return 0;
}

所以,这同样导致了类似前一种问题那样,出现重定义的问题。

解决方法:使用#ifndef/#define避免重定义

那么,如何解决这个问题呢?我们可以在每个头文件中,加上类似如下的宏语句。

比如,对于foo.h,可以添加:

代码语言:javascript
复制
#ifndef __FOO_H__
#define __FOO_H__

struct Point{int x, y;};

#endif

那么,#ifndef/#define是如何工作的呢?

#ifndef FOO_H .... #endif表示:如果未定义__FOO_H__,则将其后的代码纳入编译中;否则,就跳过 #define FOO_H__表示:预定义宏__FOO_H

这里以示例二给出的问题为例,介绍如其是如何发挥作用的:

  1. 当C编译器第一次包含foo.h头文件时
代码语言:javascript
复制
#include "foo.h"        // 第一次包含foo.h
#include "bar.h"   

int main() {
    struct Point p;
    p.x = 1;
    p.y = 2;
    return 0;
}

预处理器的执行顺序如下:

  1. 遇到#ifndef FOO_H,检查宏__FOO_H__是否存在:由于此时,__FOO_H__未定义,因此条件成立。
  2. 执行#define FOO_H,宏FOO_H被定义: 之后的代码 struct Point { int x, y; }; 被纳入编译。
  3. 遇到#endif,结束条件编译块。

结果就是,预处理器将struct Point放入 main.c 中,供后续编译阶段处理,相当于如下效果:

代码语言:javascript
复制
struct Point{int x, y;};
#include "bar.h"   

int main() {
    struct Point p;
    p.x = 1;
    p.y = 2;
    return 0;
}

接下来,C编译器处理#include "bar.h",由于bar.h中使用了#include "foo.h"语句,因此处理后的效果为:

代码语言:javascript
复制
struct Point{int x, y;};
#include "foo.h"
int main() {
    struct Point p;
    p.x = 1;
    p.y = 2;
    return 0;
}

最后,C编译器继续处理#include "foo.h",此时:

  1. 遇到#ifndef FOO_H,检查宏__FOO_H__是否存在:由于此时__FOO_H__已经定义,因此条件不成立。
  2. 条件不成立,预处理器跳过#define和整个头文件内容,包括struct Point。
  3. 遇到#endif,结束条件编译块。

因此处理后的效果为:

代码语言:javascript
复制
struct Point{int x, y;};

int main() {
    struct Point p;
    p.x = 1;
    p.y = 2;
    return 0;
}

这样一来,虽然foo.h被直接直接或间接包含两次;但最终struct Point只被展开一次,这样就避免了重复定义。

综上所述,使用#ifndef/#define/#endif,可使得第一次包含头文件时,条件成立,相关的宏被定义且头文件内容被展开。 而第二次及之后包含时,由于宏已经定义过了,使得条件不成立,头文件内容被跳过。 也就说,头文件保护通过宏定义状态让编译器记住已经处理过这个头文件,从而安全地多次包含头文件,避免重复定义变量、结构体或函数声明。

历史文章

课程推荐

作者介绍

李述铜,嵌入式系统与底层架构领域讲师,专注于操作系统、CPU 架构的教学与研究。 出版作品《从0手写x86计算机操作系统》。主讲课程包括:《从0手写嵌入式操作系统》《从0手写TCP/IP协议栈》等。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、问题场景:如果不使用#ifndef/#define会怎样?
    • 示例一:简单问题示例
  • 示例二:复杂问题示例
  • 解决方法:使用#ifndef/#define避免重定义
  • 历史文章
  • 课程推荐
  • 作者介绍
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档