前言
在我们用C语言编程的时候,会遇到这么一个问题。
A文件用到了一个功能,B文件也用到了这个功能,那对于初学者来说,就只能将A文件实现这个功能的代码粘贴一份到B文件。之后如果C文件D文件,也需要这个功能,也只能粘贴代码了。不得不说,这很麻烦。
举个实际的例子,用C语言作数据结构练习的时候,需要输出线性表这个功能:
c
void printList(int *data, int length)
{
for (int i = 0; i < length; i++)
{
printf("%d ", data[i]);
}
printf("\n");
};
这个功能的使用频率是非常高的,在每个文件都复制这样一份代码是很不划算的一个做法。
可不可以将这个代码放在一个文件里,然后其他文件要用到这个函数,直接引用就好了,没必要直接复制代码。
当然是可以的,C语言提供了这一功能。
从其他文件中引入自定义函数
假设现在有个index.c
文件, 这个文件的就是要将data
数组中的内容打印出来
c
#include <stdio.h>
void printList(int *data, int length)
{
for (int i = 0; i < length; i++)
{
printf("%d ", data[i]);
}
printf("\n");
};
int main()
{
int data[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
printList(data, 10);
return 0;
}
现在编译这个文件:
没有任何提示就是不报错了,
-o 表示编译后的文件名是
index
,如果不指定文件名,就会默认a.out
运行一下:
正确输出,没有问题
在index.c
中,负责打印的功能是函数printList
,现在将printList
放到util
文件中去:
c
//uitl.c
#include <stdio.h>
int printList(struct List *L)
{
for (int i = 0; i < L->length; i++)
{
printf("%d\n", L->data[i]);
}
return 0;
};
index.c
文件只剩下main
函数了,除此之外,还需要保留printList
的函数声明:
函数声明就是含有函数返回类型,函数名称,函数的参数;
函数的定义就是带有函数体的函数声明
在
util.c
中的printList
函数就是一个函数定义
c
//index.c
#include <stdio.h>
int printList(struct List *L);
int main()
{
int data[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
struct List list;
list.data = data;
list.length = 10;
printList(&list);
return 0;
}
这样就可以了,我们来编译下index.c
文件
报错了。它说printList
这个标识符未定义(undefined symbols: printList),也就是说只有声明,但找不到uitl.c
中的printList
函数的定义。
这说明gcc命令并不会帮我们自动寻找util.c
文件,需要我们手动告诉gcc
在命令行中加入了util.c
后,编译就不报错了。咱们来执行一下:
运行成功
大致的过程就是这样,只是成熟的开发一般不这么干。想想,如果有数十个函数需要从其他文件中引入,那就需要在每个文件的前面加上数十个函数声明吗,这也很累人啊。
头文件
我们可以将函数声明放在一个头文件里面,然后再头文件引入头文件就可以了。 头文件就是以.h
结尾的文件,里面主要放一些函数声明。 现在创建一个util.h
头文件
c
// util.h
void printList(int *data, int length);
然后在index.c
引入这个头文件:
c
//index.c
#include <stdio.h>
#include "util.h"
void printList(int *data, int length)
{
for (int i = 0; i < length; i++)
{
printf("%d ", data[i]);
}
printf("\n");
};
这样就可以了,编译命令和上面一样,需要告诉gcc,去util
中找函数定义,不然又会出现undefined symbols: printList
这样的错误
你们注意到没有,
index.c
中还有一个头文件,stdio.h
,这里面其实也放着大量的函数声明。stdio
大致意思是standard I/O
,I/O
就是input/ouput
的缩写,整体意思就是标准输入输出流当我们需要在屏幕上输出内容,或者需要在从键盘输入的时候,这个头文件的引入就是必不可少的。
头文件原理
在C文件编译之前,其实还有一个预处理的过程,对于头文件来说,预处理会将头文件里面的内容插入到#include
所在的地方。我们来看看:
-E 的作用就是将预处理之后,立即停止翻译过程,不进行后续的编译过程,并且显示预处理的内容
这是index.c文件的预处理输出截图,因为有stdio.h
头文件,所以内容很多,我只截了其中一部分。
重点关注输出内容的后面,后面有我们的源代码,源代码上面就是util.h
的内容了。 看到这里,相信大家对头文件的作用理解更深刻了吧
链接
在gcc经过对文件的预处理,编译之后,会得到一个目标文件(通常以.o
后缀结尾)。
编译的作用是对C语言源文件的内容进行语法分析,进而生成对应的机器指令。但是,对有些函数来说,它的解析是无法完成的。因为它们可能并不是在当前源文件内定义的。也就是说目标文件中还没有函数的定义,需要在链接这一步才会去找函数的定义。
所以这个目标文件不能直接执行,还需要经过链接这一步。
下面将之前的编译命令拆开来看:
-c 表示仅输出目标文件
现将index.c
文件编译成目标文件index.o
目标文件是二进制文件,里面就是一大堆的0和1。用编辑器打开,大概长这样:
编辑器对二进制文件做了显示的优化,将二进制用十六进制表示,并且右边会显示二进制对应的字符内容
然后将util.c
编译成util.o
最后将两个目标文件链接起来
运行没问题,搞定
总结
这篇文章讲了如何在C语言中,引用其他文件中自己编写的函数,过程中,串讲了一些编译和链接的概念。思路清晰,例子详细,是个不错的文章。
本篇例子运行机器是Mac,window系统可能看起来不同,但其中原理是一样的