前言
(1)今天看到一个有一个头文件写上了#pragma once,刚开始有点懵。后面发现这个也是头文件防止被重复包含的一种写法。
(2)然后我打算写一篇关于头文件防止重复包含的博客。写着写着,突然就想到了,为啥要防止头文件重复包含。
(3)不知怎么的,就追溯到了c工程编译里面去了。本文将会深入介绍C程序的#include和头文件。并且介绍c工程的两种防止头文件被重复包含的写法。
为什么需要防止头文件重复包含
头文件中一般都含有什么
(1)在讲解头文件包含的两种写法之前,我们需要先知道,为什么防止头文件重复包含?
(2)首先,我们需要知道,C工程中,头文件一般会放置哪些元素。就我的个人经验来说,一般头文件只会放五个东西。
// 头文件包含 #include "stm32f10x.h" // 宏定义 #define PI 3.14159 // 函数声明 int add(int a, int b); int subtract(int a, int b); int multiply(int a, int b); double divide(double a, double b); //extern申明外部变量 extern int global_variable; // 只是声明,不是定义 // 结构体类型定义 typedef struct Point { int x; int y; } Point;
深入理解#include和头文件
实操1—正常工程文件写法
(1)我们都知道,一个工程中会存在很多个c文件和h文件。C语言我们规定了c文件中负责编写逻辑代码,h文件负责进行一些申明。
(2)我们C文件通过h文件获取一些申明信息,比如main.c需要获得test.c中的add()函数,我们只需要使用#include "test.h"就可以包含test.c中的add()函数。
(3)使用gcc编译之后发现,这种常规写法是没有问题的。
/************** mian.c **************/ #include "test.h" int main() { add(3,4); return 0; } /************** test.h **************/ int add(int a,int b); /************** test.c **************/ int add(int a,int b) { return a+b; }
实操2—工程文件没有一个头文件
(1)现在我们更改写法,假设我们不用.h文件,而是直接在main.c里面上面写一个函数声明。
(2)编译通过,运行成功。所以我们可以看到,一个工程文件,可以不需要头文件。
/************** mian.c **************/ #include "test.h" int add(int a,int b); int main() { add(3,4); return 0; } /************** test.c **************/ int add(int a,int b) { return a+b; }
头文件有啥用
(1)通过上面这个例子,我们知道,一个工程没有头文件也可也正常运转。那么需要一个头文件做什么呢?
(2)因为上面的代码比较少,所以看不出头文件的作用。假设,我们在开发一个大型的项目,里面肯定会有很多函数调用。如果没有头文件,那么在编写一个c文件的时候,都需要在上面写一大堆的函数声明。如下
int add(int a, int b); int subtract(int a, int b); int multiply(int a, int b); double divide(double a, double b); int main() { int a=4,b=6,c; c = add(a,b); c = subtract(a,b); c = multiply(a,b); c = divide(a,b); return 0; }
(3)所以说,我们可以看到,头文件作用就是存放函数申明的。说白了,头文件就是一个C文件的目录。我们只需要看一下头文件,就可以知道对应的C文件大概实现了一些啥。
(4)但是我们知道,头文件一般不只有函数声明还有结构体定义,extern声明外部变量,宏定义。这个也可以理解为目录的一部分信息。我们只需要看一下头文件的,就大体知道对应的C文件有一些啥。
头文件命名
(1)我们知道了,头文件其实就是一个C文件的目录,那么头文件命名有什么讲究吗?
(2)当然是有的。一般来说,main.c是没有头文件的,因为我们的主要业务程序都是在main.c中进行,所以他不需要专门配置一个头文件。
(3)但是我们都知道,为了让程序更好移植,都推崇模块化设计思想。所以,我们的每一个其他模块文件,都需要配置一个头文件。当我们拿到这个模块的时候,只需要看一下他的头文件都有一些啥。就大体知道需要怎么使用了,至于底层的实现,等出现bug的时候再研究。
(4)因为头文件是为了描述一个C文件的,所以规定头文件和C文件名字要一样。比如给我们的C文件是OLED.c,那么他的头文件就应该是OLED.h。
(5)这个时候,可能有些叛逆骚年想问,我OLED.c的头文件命名为nb.h可以不?答案肯定是可以的,只要不怕被打。
#include做了什么?
(1)现在我们知道了头文件是一些啥了,现在我们看看#include做了什么。
(2)使用gcc -E指令,我们可以看到C文件的预编译之后的结果。通过下面的结果,我们可以看到,#include本质就是将后面包含的文件内容拷贝过来。
(3)可能还有一些人还想让我说一些什么,但是的确没有可以讲的了。(苦笑)因为#include说白了就是进行一次拷贝。
实操3—工程文件存在一个头文件被重复包含
(1)现在我们已经了解了,头文件和#include的作用之后,现在再次扩展。我们在正常的开发中,一个头文件肯定会被多次包含的。就拿stdio.h文件为例子,这个头文件中包含了printf函数的声明,所以绝大多是,C文件都需要使用#include <stdio.h>进行头文件包含。
(2)我们上面知道了#include其实就是对头文件进行拷贝,如果我们的main.c使用包含了b.h和a.h,而a.h又包含了b.h。这样就会出现重复包含的问题。
/************** mian.c **************/ #include "a.h" #include "b.h" int main() { int result = add(three, 4); return 0; } /************** a.h **************/ #define three 3 int add(int a, int b); extern int x; /* struct student{ char* name; int age; char* sex; }; */ /************** b.h **************/ #include "a.h"
(3)现在我们使用gcc进行编译会发现,可以成功编译,再进行运行。结果会看到,也可以正常运行。
(4)可能有些人会有疑惑了,什么鬼,不应该会出现头文件被重复包含的报错吗?
(5)非常不幸,不会的。对C文件变成可执行文件的流程有一点点了解的人会知道。C文件到可执行文件需要经过,预处理,编译,汇编,链接这四个过程,而我们的语法检测是再编译期间。那么就存在一个问题,如果C文件经过了预处理,最终产生的C文件符合语法就不会产生报错!
(6)现在我们来看看main.c经过预处理之后的样子吧。我们会发现预处理其实就做了两件事:
<1>让three变成数字3
<2>然后将函数声明和extern拷贝两次放在test.i中。
(7)虽然函数声明和extern被重复写了两次,但是这样写是符合C语言语法的。所以如果头文件中只有宏定义,函数声明和extern,不写条件编译也是不会进行报错的。
(8)但是我个人建议所有头文件还是写上条件编译的。因为,虽然你文件不会进行报错,但是那样会减少编译效率,会导致编译器多次读取和处理相同的代码,增加了编译时间和开销。
(9)但是有些人要问了,为什么我感觉我的头文件如果没有写上条件编译,就会报错呢?现在我们在头文件中加入结构体定义,就马上出现报错了。
/************** mian.c **************/ #include "a.h" #include "b.h" int main() { int result = add(three, 4); return 0; } /************** a.h **************/ #define three 3 int add(int a, int b); extern int x; struct student{ char* name; int age; char* sex; }; /************** b.h **************/ #include "a.h"
小结
(1)头文件其实就是一个目录,方便我们阅读模块的作用。一般存放头文件包含,宏定义,函数声明,extern外部变量声明,结构体类型定义。
(2)头文件命名要和对应的C文件名字一致,也可以不一致,只要不怕被打。
(3)#include本质就是将后面包含的文件内容拷贝过来。
(4)如果头文件中只含有头文件包含,宏定义,函数声明,extern外部变量声明,就算不进行条件编译,也不会出现语法错误。但是不建议,因为这样会降低编译效率。
防止头文件重复包含的两种写法
前面说了,头文件建议都加上条件编译。这里将会介绍两种条件编译的写法。
#ifndef #define #endif
(1)想必绝大多数人都只知道这一种条件编译写法。这个是C库规定的条件编译。
(2)各位写条件编译一般都是按照下面这种格式来写的。但是各位有没有考虑过,为什么b.h文件的条件编译是__b_H_吗?我可以改成别的吗?
(3)答案肯定是可以的,这个其实也是程序员们的默认规定。你的编译条件改成__nb_H_也行的,也是条件编译,只是皮糙肉厚就行,被打的时候声音小点。
/************** 标准写法 **************/ /************** b.h **************/ #ifndef __b_H_ #define __b_H_ //头文件的内容 #endif /************** 不怕打写法 **************/ /************** b.h **************/ #ifndef __nb_H_ #define __nb_H_ //头文件的内容 #endif
#pragma once
(1)这个绝大多数人应该都是没有接触过的,因为这个并不是C语言规定的写法。他不保证能够在所有编译器中支持,所以你使用他的时候,可能会进行报错。
(2)这个写法就很简单了,只需要在头文件的第一行写上#pragma once,那么编译器就会自动识别,然后当前头文件只会编译一次。
/************** b.h **************/ #pragma once //头文件的内容
进阶学习#include
(1)前面说了,#include其实就是在预处理阶段将后面的文件内容拷贝到当前文件。那么,#include后面只能是.h文件吗?
(2)当然不是,#include后面你想是什么文件都可以。
进阶学习头文件
让.h文件编写c程序
(1)本文都说了,深入理解头文件,如果只是前面这么一点点内容。无疑是标题党。那么,现在我们开始上干货,头文件真的只能写我上面说的那五个内容吗?
(2)我都这么问了,答案肯定是否定的。我们上面知道了,#include实际上就是将后面的文件内容拷贝当当前文件,那么我们程序是不是能够这么写?
/************** test_h.h **************/ #include <stdio.h> int main() { printf("hello\r\n"); } /************** test_h.c **************/ #include "test_h.h"
(3)编译显示是可行的,为什么呢?依旧是那句话,C文件到可执行文件的四个步骤里面,只有编译阶段才会进行语法检测。那么,在预编译阶段#include "test_h.h"将test_h.h的代码拷贝到test_h.c中了,然后在编译阶段,他看到的是test_h.c中有c程序。毫无疑问,是不会存在问题的。
不要c文件,全是.h文件进行编译
(1)已经研究到这里了,我们再大胆一点。我们让这个工程里面没有c文件,只有.h文件进行编译,看看会有什么效果。
/************** test_h.h **************/ #include <stdio.h> int main() { printf("hello\r\n"); }
(2)我们会发现,虽然工程中没有c文件,只有头文件进行编译是没有问题的。但是却无法执行,使用file指令查看文件,他提示是GCC预编译的C头文件(版本014)。
(3)到此为止,可能有些人就认为可以结束了。但是,这样还够深入吗?如果到这里就截至了,显然没点意思。
(4)在肯哥的交流群中抛出这个问题只会,我发现肯哥进行gcc编译,是使用的gcc -o test_h -xc test_h.h这种写法。因为不知道-xc是什么,使用gcc --help查看一下。
(6)我们能够看到-x的解释:指定下列输入文件的语言。允许的语言包括:c、c++、汇编、无’none’表示恢复到的默认行为根据文件的扩展名猜测语言。
(7)重点看我加粗的部分,他说了,如果没有指定编译成什么类型语言,就根据文件扩展名来猜测。既然如此,我们加上指定编译成什么文件试试。
(8)我们指定gcc编译的工程之后会发现,文件可以运行了!因此,我们可以得出结论,.h文件不仅仅只能写上面指定的那5个内容,他写任何东西都可以,C程序也行。
(9)由此,我们可以得出结论,一个c工程可以只有.h文件,并不影响。
用任意后缀文件编写c工程
(1)这里我们会发现,只要内容不变,随便你改变文件名字。只要你指定gcc将文件以c文件方式编译即可。都可以编译通过,最终的文件都可以成功运行。
(2)既然如此,可能就会有一些人要说了,那我在windows里面用.py文件编写c工程文件,然后编译。
(3)这里明确说明,如果是开IDE编译,大概率是会报错的。因为开IDE进行编译,你IDE会根据文件后缀判断是什么语言,然后进行编译。
总结
其实这些文件后缀就是一个标识符,用于表示这个文件是个什么类型。但是如果你在Linux中,这个后缀可以随便自己起名字,反正有操作空间,让编译器重新回来。(windows中可不可以这么操作不清楚)