为什么要使用声明与定义分离,它解决了什么问题
1.代码顺序的问题
#include <stdio.h>
/*
由于函数定义顺序产生的一个编译的错误
*/
int main()
{
demo();
return 0;
}
float demo()
{
return 1.00;
}
2.解决了代码的互相依赖的问题,在a方法中调用了b方法,在b方法中又调用了b方法
#include <stdio.h>
int demo()
{
test();
}
int test()
{
demo();
}
int main()
{
demo();
return 0;
}
如下代码为什么会报错
#include <stdio.h>
/*
由于函数定义顺序产生的一个编译的错误
*/
int main()
{
demo();
return 0;
}
float demo()
{
return 1.00;
}
1.编译器看到不认识的函数调用(函数未定义),认为它会在之后的源文件中找到这个函数的详细信息。它会先记录下来,随后在文件中查找
2.编译器需要知道函数的返回类型,由于函数未定义,所以不知道它的返回类型,所以只好假设它返回int
3.等编译器看到实际的函数,返回"conflicting types for 'demo'"的错误,因为编译器认为有两个同名的函数,一个是文件中的函数,一个是编译器假设返回int的那个
什么是声明与定义分离
如果在第二步函数就知道返回类型了,就不需要再找了,所以为了防止编译器假设函数的返回类型,可以显示的告诉他,告诉编译器函数会返回什么类型的语句就叫函数声明;一旦声明了,编译器就不需要假设了,完全可以先调用函数,再定义函数
声明只是一个函数签名:一条包含函数名,形参类型与返回类型的记录
如何让声明与定义分离
例如:
#include <stdio.h>
/*
此处声明了函数
*/
int demo();
int main()
{
demo();
puts("this is main function");
return 0;
}
/*
此处定义了函数
*/
int demo()
{
puts("this is demo function");
return 0;
}
创建完全和函数体分离的头文件
demo.h文件内容:
int demo();
函数体中的内容:
#include <stdio.h>
/*
注意这里是双引号,且没有分号,文件在当前目录下,是相对路径,里面之定义了头信息(就是函数的返回值以及类型)
*/
#include "demo.h"
int main()
{
/*
调用未定义的函数,不报错(因为上面加载了函数头信息)
*/
demo();
puts("this is main function");
return 0;
}
int demo()
{
puts("this is demo function");
return 0;
}
什么是预处理
预处理是把c源码转化成可执行文件的第一个阶段,预处理会在编译开始之前修改代码,创建一个新的源文件(为了提高效率,编译器通常会用管道在两个阶段之间发送数据)
include <> 和include “”有什么区别
引号表示使用以相对路径查找头文件,如果不加目录名,只包含一个文件名,编译器就会在当前目录下查找头文件;如果遇到尖括号,会到标准库代码所在目录查找头文件
gcc一般会去哪些位置寻找头文件
gcc知道标准库的头文件被保存的位置,头文件一般保存在/usr/local/include,/usr/include这些地方
编译器是如何工作的
1.预处理:修改代码
用include指令添加相关头文件;跳过程序中的某些代码;补充一些代码
2.编译:转换成汇编代码
计算机只理解底层的机器代码指令,在这一步,会把语句或函数调用转化成一串汇编语言指令
3.汇编:生成目标代码
编译器将符号代码汇编成机器代码或目标代码,也就是CPU内部电路执行的二进制代码
4.链接:放在一起
一旦有了目标代码,就需要将它们拼在一起,构成可执行程序,当某个代码调用另外一个目标代码的函数时,编译器会把它们连接在一起。同时,链接还会确保程序能够调用库代码。最后程序会写到一个可执行程序文件中,文件格式视操作系统而定,操作系统根据文件格式把程序加载到存储器中运行
如何进行代码共享
例如在将数据写入到数据库中需要进行加密,在将数据写入到缓存中中也要进行加密,他们的加密方法是一致的,不需要在两个文件中都写一份加密的代码
1.创建自己的头文件
encrypt.h文件
void encrypt(char *message);
2.引用头文件
encrypt.c文件
#include "encrypt.h"
void encrypt(char *message)
{
char c;
while(*message) {
*message = *message ^ 31;
message++;
}
}
db.c文件
#include <stdio.h>
#include "encrypt.h"
int main()
{
char msg[80];
while(fgets(msg, 80, stdin)) {
encrypt(msg);
printf("%s\n", msg);
}
}
3.执行命令(将多个文件编译成一个可执行文件)
gcc db.c encrypt.c -o db && ./db
如果源文件很多,每次编译都需要耗费很长的时间,如何做到只重新编译改了的文件
回顾下编译器的工作原理,如果源文件没有进行变化的话,那么只有最后一步需要重新做(也就是链接);如果文件修改了毫无疑问需要重新编译
如何做能让gcc把目标代码保存在文件中?然后让编译器把目标文件链接起来?
1.首先把源代码编译为目标文件
gcc -c *.c
2.把目标文件链接起来
gcc *.o -o demo
修改代码的编译流程变成了这样
1.首次编译
gcc *.c -o demo
2.修改了test.c文件
3.再次编译
gcc -c test.c
4.再次链接
gcc *.o -o demo
如果我修改了很多文件,记录下来修改哪些文件也很麻烦
使用make工具自动化构建
make
make是一个可以运行编译命令的工具。make会检查源文件和目标文件的时间戳,如果目标文件过期,make就会重新编译它
需要提供给make的条件
1.依赖项;生成目标需要用哪些文件
2.生成方法;生成该文件时要用哪些指令
例如
将demo.c编译成demo.o,依赖项和生成方法分别是什么?
demo.o就是目标,因为需要生成这个文件,demo.c就是依赖项,因为编译器创建demo.o的时候需要它
生成方法就是将demo.c转化成demo.o的命令也就是gcc -c demo.c
怎样把依赖项和生成方法告诉make
所有目标,依赖项和生成方法的细节信息需要保存在一个叫makefile或Makefile的文件中
拿加密的例子说:
最终的加密程序db由encrypt.o和db.o文件链接组成,这两个文件是由C文件和头文件编译而成,db.o依赖encrypt.h文件,因为db.o需要调用encrypt.c中的函数
db.o: db.c encrypt.c encrypt.h
gcc -c db.c
encrypt.o: encrypt.c encrypt.h
gcc -c encrypt.c
db: db.o encrypt.o
gcc db.o encrypt.o -o db
创建makefile很麻烦
使用autoconf来自动生成makefile