c++Analyse-类模板编译过程

1C++编译链接全过程

C++的编译过程分为预编译、编译、汇编、链接

1.1 预编译

预编译过程会进行一些基本的操作
操作1:将会把#define宏定义进行替换

#define Max 100
//如程序中出现了上面的语句,则预编译结束后,程序中的所有 Max 都已经被替换成了100

操作2:执行条件编译:#ifdef,#ifndef,#else,#elif,#endif 等语句

#ifndef XXX
#define XXX
#endif
/*
其实上面的语句相当与一个常用的if判断语句,只不过这个判断语句实在在预编译阶段完成对的
*/

操作3:对#include宏进行替换。也就是把 #include所引用的头文件中的内容原封不动的插入在当前行的位置并把当前这一行替换掉

//main.cpp文件内容
#include "my.h"
void main()
{
}

//my.h文件内容,有一个变量声明和一个函数声明
int age;
void show(void);

//预编译后
int age;		
void show(void);
void main()
{
}

操作4:删除注释,就是加了//和/* */部分
操作5:添加行号和文件名标识

1.2 编译

编译过程将会检查程序中的语法错误,若没有错误则会将程序转换成汇编语言。
词法分析:将源代码的字符序列分割成以系列的记号
语法分析:对记号进行语法分析,产生语法树
语义分析:判断表示式是否有意义
代码优化
目标代码生成:生成汇编代码
目标代码优化

  • 单个文件编译
    正常编译即可

  • 分文件编译
    C++编译的一个特点就是 分文件编译 ,也就是每一个.cpp文件都会单独进行编译。
    也就是说每一个.cpp文件都会单独生成一个目标文件。
    但是这样就出现了一个问题:
    如果一个木目标文件中的函数调用了另一个目标中的函数怎么办呢??
    一个项目,假设预编译之前是这样子的:
    在这里插入图片描述
    可以看到,main中调用了Person.cpp文件中的func函数。
    经过预编译之后会变成下图的样子:
    可以看到,main中调用了Person.cpp文件中的func函数。
    经过预编译之后会变成下图的样子:
    在这里插入图片描述
    经过预编译之后,.cpp文件中的 #include “Person.h” 语句都已经被Person.h中的内容给替换掉了。这会让两个.cpp文件分别生成成两个临时文件。
    然后对两个文件分别进行编译。
    但是在编译过程总发现,临时文件2中调用了func()函数,因为该临时文件中有func函数的声明,编译器可以正常进行语法检查(输入参数和返回值类型等),虽然该文件中并没有函数实现,但这并不会报错,编译器会认为该函数的函数实现被写在了别的文件中。
    在其他文件中寻找函数实现的操作会被交给链接器。于是在临时文件2中func()函数会变成一个 特殊符号 ,等待链接器寻找其函数实现。

1.3汇编

这个过程主要是将汇编代码转变成机器可以执行的指令

1.4链接

链接过程就是将各个文件最重链接成一个可执行.exe文件的过程。
首先需要合并文件,将各个".obj"文件合并为一个文件,然后找到编译阶段生成的 特殊符号 的实现。最终生成一个可执行文件。
链接分为静态链接和动态链接

  • 静态链接:
    是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算把静态库删除也不影响可执行程序的执行;
    生成静态的链接库,Windows下为.lib为后缀名,Linux下为.a为后缀名
  • 动态链接:
    是在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函数,生成的可执行文件没有函数代码,只包含函数的重定位信息,所以当删除动态库时,可执行程序就不能运行。
    生成动态的链接库,Windows下为.dll为后缀名,Linux下为.so为后缀名

2 类模板编译过程

首先模板就是模板,不是数据类型,类模板像是一个创建类的函数,只不过这个函数的输入参数是 数据类型 。
在这里插入图片描述

2.1 类模板分文件编写存在的问题

知识储备:
1.编译器使用模板,通过更换模板参数来创建数据类型,这个过程就是 模板实例化
2.从模板类创建得到的类型称之为 特例
3.模板能否实例化创建特例,不仅仅需要模板的声明,而且还需要模板的定义,以及是否有模板的参数列表,三者缺一不可。因为只有在调用之时才有模板参数列表,所以模板的实例化是迟钝的。
4.对了,还有所以函数、变量、类等,都必须先声明再定义最后调用,这毫无疑问。
5.若我们不重写类模板的构造方法(使用编译器提供的默认构造方法),同时在其他文件中也不调用模板类的成员方法,是不会报错的。因为,这样做,我们就并没有使用到其他.cpp文件中的东西,自然不会报错。

  • 单个文件编写
    编译过程:
    由于在同一个文件中,在编译阶段,编译器首先会找到类模板的声明(用于检查语法错误)。
    然后会找到模板的定义。最后找到类模板的使用,会将其实例化。很简单的过程。

  • 分文件编写
    首先,我们一般会重写模板类的构造方法,用来进行一些操作(属性赋初值等),所以以下为重写构造方法的情况。
    经过预编译程序会由
    在这里插入图片描述
    变成下图
    在这里插入图片描述

然后对两个文件分别单独进行编译。

首先,在临时文件1中,有类模板的声明和定义,语法上无错误,但是没办法创建成员函数,也没有具体的模板参数列表,所以类模板中的成员函数此时并没有创建。

然后,在临时文件2中,虽然有类模板的声明,也有函数调用(构造函数),按道理,在这个时候应该创建并调用成员函数。但是因为该文件中没有函数定义(函数定义在临时文件1中),所以无法创建成员函数,也就无法调用。

可是,这并不会报错,因为,编译器认为,成员函数的定义在其他文件中,编译器会把该工作交给链接器来完成,编译器则会使用一个特殊符号来代替这个函数。

接下来是链接阶段,问题就出现了。

由于临时文件2中使用到了一个该文件中不存在的函数,所以链接器会在其他文件中来寻找该函数的定义。但是在临时文件1中,虽然有函数的定义,但是并没有类模板的参数列表,没有参数列表类模板的成员函数是无法创建的,所以在临时文件1中链接器也没有这个函数。所以就会出现未定义符号的错误。

2.2 解决办法

方法1:在main函数中直接包含.cpp文件,即把person.h,改为person.cpp
方法2:将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名
称,并不是强制。将person.h改为person.hpp