C语言声明与定义分离

为什么要使用声明与定义分离,它解决了什么问题

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
坚持原创技术分享,您的支持将鼓励我继续创作!