普通类成员函数编译和链接
编译
普通类成员函数可以将函数声明和函数定义分别放在.h头文件和.cpp源文件。在编译时,每个.cpp文件是一个编译单元,各个编译单元互相是不可知的。编译器会将.cpp文件中使用的头文件展开,然后经过编译、汇编将源文件生成.obj目标文件(机器代码)。
链接
每个.obj都有一个未解决符号表和导出符号表。未解决符号表是源文件使用的外部符号,需要连接器告诉其定义。导出符号表是源文件中对外可见的符号。连接器以一组目标文件和命令行在参数作为输入,生成一个完全连接的、可以加载和运行的可执行文件。连接时主要完成符号解析和重定位两个任务。
符号解析:目标文件定义和引用的符号,每个符号对应于一个函数、一个全局变量或一个静态变量。符号解析的目的就是将每个符号的引用与一个符号的定义关联起来。
重定位:编译器和汇编器生成的地址从0开始。链接器通过把每个符号定义与一个内存位置关联起来,修改这些符号的引用,使得它们指向这个内存位置。
一个常见的链接错误:error LNK1120: 1 个无法解析的外部命令 。原因就是链接时,符号引用找不到符号的定义,比如我们声明了函数却没实现。
模板类成员函数编译和链接
编译问题
首先我们要明确类模板并不是类和成员函数的定义,模板只是说明如何生成类和成员函数。在编译的时候,编译器发现类模板并不立即产生代码。只有在类模板实例化为模板类的时候(例如,创建了一个模板类对象),编译器才会产生特定类型的模板实例。
template<typename T>
class Queue
{
public:
Queue(){}
~Queue(){}
};
上面声明类模板Queue是一个生成类的方案,并不是生成类的具体定义,不会产生类代码。
Queue<int> qu1;
Queue<double> qu2;
看到上述声明后,编译器将按Queue模板来生成两个独立的类定义。类声明Queue将使用int代替模板中所有的T,而类声明Queue将使用double代替模板中所有的T,也称模板实例化。
因此,若将成员函数声明与定义分开放在.h和.cpp中。在编译时,使用类模板的源文件会将其头文件展开。在遇到模板实例化时,会生成相应的模板定义。但源文件之间时相互独立的,因此并不会实例化类模板函数(源文件中只是成员函数的生成方案)。也就是说实例化时候只会将可见部分实例化,头文件会展开(是可见的),会实例化;源文件内容独立(不可见),不会实例化。因此,实例化后生成的模板类对象调用成员函数,则会找不到函数的实现,也就会出现常见的error LNK1120: 1 个无法解析的外部命令。
解决办法
一个常见的解决方法时将类模板的成员函数声明与定义都放在头文件中,这样实例化的时候,会将所有有关模板内容全部实例化。例如,在queue.h文件中定义一个队列类模板,并将成员函数enqueue的实现放在头文件中,将dequeue的实现放在源文件queue.cpp中。
queue.h如下
#pragma once
//类模板
template<typename T>
class Queue
{
//类内结构体
struct Node
{
T data;
Node *next = nullptr;
};
public:
Queue() :m_front(nullptr), m_tail(nullptr){}
~Queue(){ }
void enqueue(const T &t);
T dequeue();
private:
Node *m_front;
Node *m_tail;
};
template<typename T>
void Queue<T>::enqueue(const T &t)
{
Node *node = new Node;
node->data = t;
if (m_front == nullptr) {
m_front = node;
}
else {
m_tail->next = node;
}
m_tail = node;
}
queue.cpp如下
#include "queue.h"
template<typename T>
T Queue<T>::dequeue()
{
Node *node = m_front;
if (m_front == m_tail) {
m_front = m_tail = nullptr;
}
else {
m_front = m_front->next;
}
T data = node->data;
delete node;
node = nullptr;
return data;
}
在主函数中调用
int main()
{
Queue<int> qu1;
Queue<double> qu2;
qu1.enqueue(8);
qu1.dequeue();
return 0;
}
因为enqueue实现在.h文件中,所以定义qu1时,会将enqueue实例化,而dequeue在.cpp文件中不会实例化。因此,调用这两个函数时,enqueue会成功,dequeue会找不到定义(在源文件中只是实现方案)。