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

举报
风正豪 发表于 2023/07/31 09:49:58 2023/07/31
【摘要】 深入介绍了C程序的#include和头文件,同时分析了c工程的预处理和编译阶段内容

前言

(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中可不可以这么操作不清楚)

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。