详解C语言中头文件的作用

        大家好,先做个自我介绍,我是天蓬,欢迎阅读本篇博文。

由于本人理解能力不是很好,阅读他人文章时,常常看得晕头晕脑,这让我很是头疼,我想,世界上一定还有和我一样的人(哈哈,不是说你么笨哦)。所以,我将会立足于读者的角度,以读者思维来进行讲解,如果你阅读完后觉得还不错的话,可以点个赞鼓励一下,当然如果对你没有任何帮助,你也可以在评论区吐槽我,让我意识到有哪些地方需要改进。

        好了,我们步入正题,今天来说说C语言中的头文件的作用,到底什么时候需要我们自己写一个头文件,如何来写,以及动态库和静态库的扩展,带着这些疑问我们往下看。


目录

1.什么时候需要头文件

2.如何自定义头文件,以及定义头文件时的注意事项

3.扩展:动态库与静态库

4.总结 


 1.什么时候需要头文件

        首先,来看一个最基本的程序。

#include <stdio.h>

int main(int argc, char const *argv[])
{
    printf("hello world");
	return 0;
}

       这里要是没了头文件,一定会编译不过。理由很简单,我们使用了printf()函数,而这个函数就是在#include <stdio.h>这个头文件中定义的。只不过,这个头文件和这个函数并不需要我们去编写,这是由于在C语言标准中就已经帮我们实现好了的,当然C语言标准中并不只是提供了这一个头文件。当我们需要使用某个函数我们去查询对应的头文件,然后引入即可(linux中的man手册是个好动西)。

        所以,通过这里我们知道,什么时候需要头文件?当然是我需要调用某个函数时,而这个函数是已经定义好的,我们直接引用对应的头文件即可(相当于将头文件中的内容引入到当前文件,如果你还不熟悉头文件或者没有尝试过自己编写,请继续往下看);


2.如何自定义头文件,以及定义头文件时的注意事项

        继续看下面的代码。

#include <stdio.h>

int add(int a, int b);

int main(int argc, char const *argv[])
{
	printf("hello world\n");

	int a=500;
	int b=20;
	printf("%d\n", add(a,b));

	return 0;
}

int add(int a, int b){
	return a+b;
}

        这里我们实现两个整数相加,定义了一个名为add的加法函数,并在main函数前声明。对于才开始学习C语言的人来说,经常会以这种方式来编写函数,但是,你有没有想过,写一个这样的函数还好,万一你要自定义一百个,一千个或者更多的函数,难道我们还是得在同一个.c中写吗,显然这样是十分不规范的。
        当我们需要自定义函数时,我们通常会将函数单独编写,并书写对应的头文件即可。这样既符合规范,提高代码的重用性,也便于我们去维护代码,毕竟,在一些复杂的项目中,一份易于维护的代码非常关键。

        那么,我们该如何将这个函数单独编写,并书写对应的头文件?

        (1)编写一个c文件,这里我叫做myadd.c,然后实现加法函数。

int add(int a, int b){
	return a+b;
}

        很简单,我们将add()函数的实现写入到myadd.c中。

      (2)编写对应的头文件,myadd.h,最好是和c文件名对应,如果你偏不这样干,那么也是完全可以的。

#ifndef _MYADD_H_
#define _MYADD_H_

int add(int a, int b);

#endif

        头文件中写入函数的声明即可。这里的ifndef,define为条件编译,是为了防止函数等被重复定义。

        (3)主函数中,引入头文件即可。

#include <stdio.h>
#include "myadd.h"

int main(int argc, char const *argv[])
{
	printf("hello world\n");

	int a=500;
	int b=20;
	printf("%d\n", add(a,b));

	return 0;
}

        整个目录结构如下:


       然后,我们使用gcc main.c myadd.c -o main 编译运行即可。

       通过以上三步,就完成了自定义头文件和源文件的编写,是不是非常简单。细心一点的读者可能会发现,myadd.c中就只有函数的实现,是不是少了点什么,的确,按照惯例来说,它缺少了对自身头文件的引用。(其实,从这里我们可以看出,头文件和源文件除了函数名称对应以外,并没有其它的任何关联,即头文件的定义和源文件是相互独立的,是两个文件,真正的关联是在函数运行过程中进行的,也就是说,我们程序运行时,当我们调用到这个函数后,才会通过你调用的函数名称去寻找对应的函数并执行(即函数的入栈),但是在编译阶段,编译器并不关心这个函数的实现,它只关心有没有这个函数,比如这里调用的add加法函数,在编译阶段,编译器找到了在myadd.c中已经定义了这个函数,编译器就认为是没有问题的,至于你有几个参数,参数是什么类型,它才不会管)。由于C语言中没有涉及多态的概念,这就从本质上决定了和C++的编译方式是有区别的,那是什么区别?

        C语言中,绝不允许同名函数的存在,即使参数不同也不行。但是C++中就可以。比如,在C语言中,写一个整数的加法和浮点数的加法,那么就意味着你必须定义两个函数名称不同的方法。而在C++中大可不必,这是由于C语言编译时,并不会去对参数做具体的判断。那么问题来了,如果不对参数做判断,那我们有一天突然在写代码时,打瞌睡写成这个样子了,会怎样?

int add(int a, int b,int c){
	return a+b;
}

        这里我们参数多加了一个,但如果你编译的话,会惊奇的发现居然通过了。但是,我们如果运行这个程序,肯定是会报错的,因为我们调用时只给函数传了两个参数,实际上它却是三个参数。所以,这样的话,如果我们稍不留神,就会导致定义和声明不一致,编译却通过了,这种问题排查起来就会比较麻烦又耗费时间,领导就会来找你麻烦了,那么有没有解决的方式?肯定是有的。

        我们在自定义头文件时,引用自己的头文件即可,这样就相当于将函数的定义和声明,放到同一个文本中【别把头文件和c文件想得多高大上,其实就是个普通的问本文件而已,相当于王麻子改了个名叫老王】,如果定义和声明不一致,在编译阶段就会报错 ,将错误扼杀在摇篮当中。如下:

#include "myadd.h"
int add(int a, int b){
	return a+b;
}

         这样,就必须保证函数的定义和头文件中的一致。并且,引用自己的头文件并不只是这个作用,我们如果需要使用头文件中定义的其它内容时,也要引用,这里的其它内容在书写头文件的注意事项中有介绍。所以,我们在源文件中正常情况下都要引用自身的头文件。有很多小伙伴都是只知道要这样写,并不知道这样写的意义是什么,或者理解得不够全面。

       这里,我简单的总结一下自定义头文件有哪些注意事项:

        a.源文件名和头文件名称保持一致(如myadd.h和myadd.c)

        b.源文件中引入自身头文件,这样避免了函数的声明和定义不一致带来的麻烦

        c.头文件中只能存在函数的原型(声明),结构体,联合体,宏,枚举,变量。不要将函数定义写在头文件中,当然这样写也不会报错,因为本质上它们都是文本,但是不报错不代表就是对的。

        d.include<>,include " "的区别,上面的例子中,我们就是使用了#include "myadd.h",这个和<>的唯一区别就在于搜索路径不同。<>代表从定义的标准库中去查找,而" "代表从当前路径查找,显然,我么定义的myadd.c在当前路径,并不在标准库中。


3.动态库与静态库

        通过前面的讲解,我们已经知道如何为源文件添加对应的头文件了。我们之所以要这样做,无非是有两个目的,一是方便自己,使得自己的代码更具有工程思维,也便于维护。二是方便他人,我们可以将写好的源文件和和头文件,提供给别人使用,这样,也可以便于合作开发。但是,现实中,我们并不会直接将源文件提供给别人,而是以库的形式来提供给第三方。

        为什么要提供库,而不是直接将源文件和头文件给别人呐?那是因为提供库比直接提供源文件有以下好处:

        a.可以保护知识产权;假如我们花费了三个月写的代码,需要提供给其他公司,但是我也并不想其它公司的人可以看到我是如何写的,这个时候就可以将代码封装成库再提供。

        b.易于迭代;假如你们公司最近开发了一个软件,并且已经批量的给客户使用了,但是,客户在使用的过程中,突然发现有bug,这个时候怎么办?当然是修改代码,然后修复后重新提供一个新的版本给客户使用就行了。但是,这就意味着,你只是修改了一小点,而用户需要更新全部,下载新的版本,这样显然不可取。可取的是,我们将函数功能封装成库,提供给用户。我们修复bug时,只需要重新生成一个新的库,而不必重新提供整个版本,这就是我们常说的打补丁了。

        c.更加高效和规范;你想,我们在开发过程中,常常会 写很多功能相同的函数,如果每一次都要重复去写,那就很费时费力了。通常的做法是,我们把这些功能相同的函数,用库封装起来,下一次编写时,就不用再去书写了,而是直接导入头文件即可,省时省力。

        上面说了这么多,也还没说库到底是个啥,我们又如何去将我们的.c生成为库呐?不急我们接着往下看。

        先来个三联问:什么是库?库的种类与区别?如何将源文件打包成库?如何使用库?下面我们围绕这几个问题来展开讲解。

      (1)什么是库?

        库又叫函数库,见名知意,它一般是我们在程序中需要反复使用的一些函数,比如,我们常写的#include <stdio.h>中就有我们常常需要使用的scanf()和printf()函数。

       (2)库的种类与区别?

        我们一般将库分为动态库和静态库;其中静态库的命名以lib为开头.a为后缀,比如libadd.a;动态库的命名以lib为开头.so为后缀,比如libadd.so。这是命名上的区别。

        其次,静态库和动态库的编译方式和载入时机都是不一样的,这里先说载入时机;静态库是在我们编译源文件时,就将静态库和源文件一起编译,生成可执行的二进制文件。 而动态库是在我们程序运行时才会去加载,如果你对C语言编译的四个阶段还不太了解的话,可以先补一下这方面的知识。刚刚说道,静态库时编译时就加载的,动态库运行时加载,那么这就意味着,我们开发的同一个程序,如果采取两种不同的方式,那么编译得到的可执行程序的文件大小一定是静态库大于动态库的,因为动态库编译时并不会加载库文件,只有 运行时才会加载。还值得一说的是,既然静态库编译时已经加载了,那是不是编译后就不需要它,程序就可以独立运行了呐?的确如此。相反,动态库是运行时才加载,所以如果程序运行时找不到这个动态库,那么一定会出现异常。在项目中要根据实际情况来决定具体要使用哪一种库,没有绝对的好坏之分。看了这些,如果你觉得有点抽象,那么你接着看看他们是如何制作与使用后,你就会恍然大悟了。

       (3)如何将源文件打包成库?

        还是沿用之前的例子,以加法为例来演示动态库和静态库的制作过程,我们这里简单介绍一下制作过程。这里还是再看看整个代码结构。

一个主函数main.c,myadd.c里面是一个加法函数,myadd.h是对应的头文件,以此为例。

        静态库:将myadd.c打包成静态库

        a.gcc myadd.c -o myadd.o -c【myadd.o为重定向文件,如果你不知道这个的话,建议先熟悉一下C语言编译的4个过程】执行后会在当前文件生成一个myadd.o的新文件。如果我们有多个.c怎么办,简单,分别执行这条命令,myadd.o换成对应的名字即可。

        b.ar cr libadd.a myadd.o 【ar 为打包工具,cr为参数,如果需要请自行查询即可,再次说明man手册是非常强大的,如果你还不会,执行一下 man ar会有关于ar命令的详解;这里的libadd.a是我们根据静态库的命名规范自定义的一个名称;如果有多个重定向文件(.o)怎么办,末尾继续追加就可】这样就得到了名为libadd.a的库函数。如图:

         动态库:将myadd.c打包成动态库

        a.gcc  -shared -fPIC  -o libadd.so  myadd.c 【-shared表示生成动态库,fPIC为参数,libadd.so为动态库的命名规范,myadd.c表示要编译的源文件,如果有多个源文件要同时编译问一个动态库,直接后面追加即可】一步到位,就是这么简单,来看看生成的文件,如图:

 通过上述过程,我们知道了如何将原文件打包为库文件,有了库文件我么如何使用呢?

        (4)如何使用库?

        我们的函数入口为main.c,里面我们调用了加法add函数,现在我么就可以不依靠myadd.c来编译代码了,因为myadd.c被编译成了库函数。现在我们删除myadd.c和myadd.o,中保留主函数,头文件和库函数,来看看如何利用库函数来编译主函数的?

        编译命令:gcc main.c -o main -I ./ -L ./ -ladd 【-I 表示头文件的路径名,头文件在同一路径,所以使用./代替,-L表示库的路径,这里也在当前路径下,所以也使用./,最后的-ladd,其中l是必须的表示需要的库,add表示去掉前缀lib和后缀.so或者.a后的库名,不能分开书写】执行后即可生成main可执行文件。

        【注意!】因为无论是使用静态库编译或者使用动态库编译我们都是使用的同一命令,所以在编译的时候最好是先将一个库拷贝出来,让这个目录下只含有一个库,或者说将其中一个放到其它文件下,我们使用-L 时加上相对路径就好。通过以上操作,我们就使用库编译好了可执行的二进制文件,现在,如果你是编译的静态库,你就可以只保留可执行的二进制文件,然后就可以正常运行了,但是,如果你是编译的动态库的话,你会发现运行的时候就会失败,这是为什么呐?这是因为,我们说过,使用静态库编译是在编译时就将源文件和库一起编译,生成可执行的二进制文件,固然它是可以独立运行的;但是,动态库是在运行时才会去加载[注意,我们编译生成二进制文件时也需要动态库的支持,而且它加载的路径是在/usr/lib或者/lib/目录下,当程序运行的时候,就会去这两个路径下去找对应的动态库了,所以我们将他拷贝到这两个目录的其中一个就可以运行了。此外,还可以添加环境变量的方式,将库放在任何目录下,这里不建议这样做,但是还是说一下,我们只需要向bashrc的末尾添加 export LD_LIBRARY_PATH=动态库的绝对路径   然后保存重启或者使用source一下就可以了,这样以后去搜索库时,除了搜索/usr/lib/,/lib/目录下,还会搜索我们刚刚添加的这个路径,这样的操作是永久生效的,如果你是想临时验证,直接执行 export LD_LIBRARY_PATH=动态库的绝对路径  这个命令就好了,如果你不知道环境变量,或者source这些概念,甚至你还不会linux基本命令,那么就需要补一下这方面的知识了。通过上面的操作,我们使用动态库编译的程序就可以运行了,现在,如果你需要升级,更改了函数实现,比如将加法里面再添加一句打印,你想想,是不是直接修改后提供给用户新的库就好了,而不需要向用户提供整个可执行文件了。


 4.总结

        前面我么说了这么多,总的来说还算详细,如果你还 遇到了其它问题,欢迎评论区和各位探讨。现在,我么们总结一下前面说的:

        (1)什么时候需要头文件:当我们使用某个库中的函数,结构体或者变量等,我么就需要使用头文件。这个头文件可以是C语言标准中实现的,也可以是自己实现的。

        (2)如何自定义头文件,以及定义头文件时的注意事项:自定义头文件时要在原文件中引入自己的头文件,这样在编译阶段,会检测到函数的声明和定义是否一致,防止错误发生在运行时阶段。头文件要有ifndef,define等条件编译的宏,这是为了防止重复定义。此外要知道头文件中不要出现函数的具体实现,只能是函数声明,宏,结构体,联合体,变量,枚举,还有内联函数,这是一种规范。

        (3)动态库和静态库:我们介绍了头文件的一些知识点,就牵涉到了我们如何将自己写的函数打包成库,这里我们需要知道库的种类,使用库和直接使用源文件有何不同,要清楚库的编译方式以及如何去使用它们。

       好了,说到这就结束了,我们江湖见。

  

  • 132
    点赞
  • 372
    收藏
    觉得还不错? 一键收藏
  • 16
    评论
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值