前言

(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;
}

深入理解C程序的#include和头文件,让c工程只有.h文件(狗头)

实操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;
}

深入理解C程序的#include和头文件,让c工程只有.h文件(狗头)

头文件有啥用

(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说白了便是进行一次复制。

深入理解C程序的#include和头文件,让c工程只有.h文件(狗头)

实操3—工程文件存在一个头文件被重复包括

(1)现在咱们现已了解了,头文件和#include的作用之后,现在再次扩展。咱们在正常的开发中,一个头文件必定会被屡次包括的。就拿stdio.h文件为例子,这个头文件中包括了printf函数的声明,所以绝大多是,C文件都需求运用#include <stdio.h>进行头文件包括。
(2)咱们上面知道了#include其实便是对头文件进行复制,假如咱们的main.c运用包括了b.h和a.h,而a.h又包括了b.h。这样就会呈现重复包括的问题。

深入理解C程序的#include和头文件,让c工程只有.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进行编译会发现,能够成功编译,再进行运转。成果会看到,也能够正常运转。

深入理解C程序的#include和头文件,让c工程只有.h文件(狗头)

(4)或许有些人会有疑问了,什么鬼,不该该会呈现头文件被重复包括的报错吗?
(5)非常不幸,不会的。对C文件变成可履行文件的流程有一点点了解的人会知道。C文件到可履行文件需求经过,预处理,编译,汇编,链接这四个进程,而咱们的语法检测是再编译期间。那么就存在一个问题,假如C文件经过了预处理,终究产生的C文件契合语法就不会产生报错!
(6)现在咱们来看看main.c经过预处理之后的样子吧。咱们会发现预处理其实就做了两件事:
<1>让three变成数字3
<2>然后将函数声明和extern复制两次放在test.i中。
(7)虽然函数声明和extern被重复写了两次,可是这样写是契合C言语语法的。所以假如头文件中只需宏界说,函数声明和extern,不写条件编译也是不会进行报错的。
(8)可是我个人主张所有头文件仍是写上条件编译的。由于,虽然你文件不会进行报错,可是那样会减少编译功率,会导致编译器屡次读取和处理相同的代码,增加了编译时间和开支

深入理解C程序的#include和头文件,让c工程只有.h文件(狗头)

(9)可是有些人要问了,为什么我感觉我的头文件假如没有写上条件编译,就会报错呢?现在咱们在头文件中加入结构体界说,就马上呈现报错了。

深入理解C程序的#include和头文件,让c工程只有.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"

小结

(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后边你想是什么文件都能够。

深入理解C程序的#include和头文件,让c工程只有.h文件(狗头)

进阶学习头文件

让.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程序的#include和头文件,让c工程只有.h文件(狗头)

不要c文件,全是.h文件进行编译

(1)现已研究到这儿了,咱们再大胆一点。咱们让这个工程里边没有c文件,只需.h文件进行编译,看看会有什么作用。

/**************  test_h.h  **************/
#include <stdio.h>
int main()
{
	printf("hello\r\n");
}	

(2)咱们会发现,虽然工程中没有c文件,只需头文件进行编译是没有问题的。可是却无法履行,运用file指令查看文件,他提示是GCC预编译的C头文件(版本014)。

深入理解C程序的#include和头文件,让c工程只有.h文件(狗头)

(3)到此为止,或许有些人就以为能够结束了。可是,这样还够深化吗?假如到这儿就到了,显然没点意思。
(4)在肯哥的沟通群中抛出这个问题只会,我发现肯哥进行gcc编译,是运用的gcc -o test_h -xc test_h.h这种写法。由于不知道-xc是什么,运用gcc –help查看一下。
(6)咱们能够看到-x的解释:指定下列输入文件的言语。答应的言语包括:c、c++、汇编、无’none’表示康复到的默许行为依据文件的扩展名猜测言语
(7)要点看我加粗的部分,他说了,假如没有指定编译成什么类型言语,就依据文件扩展名来猜测。既然如此,咱们加上指定编译成什么文件试试。

深入理解C程序的#include和头文件,让c工程只有.h文件(狗头)

(8)咱们指定gcc编译的工程之后会发现,文件能够运转了!因此,咱们能够得出结论,.h文件不仅仅只能写上面指定的那5个内容,他写任何东西都能够,C程序也行。
(9)由此,咱们能够得出结论,一个c工程能够只需.h文件,并不影响。

深入理解C程序的#include和头文件,让c工程只有.h文件(狗头)

用任意后缀文件编写c工程

(1)这儿咱们会发现,只需内容不变,随意你改动文件姓名。只需你指定gcc将文件以c文件方式编译即可。都能够编译经过,终究的文件都能够成功运转。
(2)既然如此,或许就会有一些人要说了,那我在windows里边用.py文件编写c工程文件,然后编译。
(3)这儿清晰说明,假如是开IDE编译,大约率是会报错的。由于开IDE进行编译,你IDE会依据文件后缀判断是什么言语,然后进行编译。

深入理解C程序的#include和头文件,让c工程只有.h文件(狗头)

总结

其实这些文件后缀便是一个标识符,用于表示这个文件是个什么类型。可是假如你在Linux中,这个后缀能够随意自己起姓名,横竖有操作空间,让编译器从头回来。(windows中可不能够这么操作不清楚)