在前面,我用各种容器实现过stack容器.也说了一些关于模板的基础知识.现在,我想更为深入,系统的谈谈模板代码与普通代码的区别.
首先,在面向对象编程中(注意:模板严格来说已经不属于面向对象编程了,它属于泛型编程)你看能会这么组织自己的非模板代码(这也是我推荐大家能养成的编程习惯):
- 类和其他类型被放在头文件中.通常来说,头文件是一个扩展名为.hpp(或者.H .h .hh .hxx)的文件.
- 对于(非内联)函数和全局变量,只有声明在头文件中,定义在dot-C文件中.通常而言,dot-C文件是扩展名为.cpp(或者 .C .c .cc .cxx)的文件.
这样一切就可以正常运行了,这样写也很好,我也很推荐.但是,当我们习惯了面向对象编程再转到泛型编程时你总是会感到很崩溃.甚至于你可能都不知道编译器为什么会出错,明明你写的语法没有任何问题!
举例:
头文件func.h
#ifndef __FUNC__
#define __FUNC__
//头文件里的函数声明
template<typename T>
void func(T const&);
#endif // !__FUNC__
头文件func.h的实现
#include "func.h"
#include <iostream>
template <typename T>
void func(T const&) {
std::cout << "我什么也不想做" << std::endl;
}
测试
#include <iostream>
#include "func.h"
int main()
{
func(10);
return 0;
}
如果你跟我一样使用的是visual studio 2019的话,编译器会在你尝试运行时报错.报错的意思大概是:对于func(10)这个函数的调用无法解析.换句话来说,编译器不认识这个东西.What!???你可能会莫名其妙.
实际上,这根模板的特点有关:那就是在这个文件中我被用到了我就实例化(这种实例化是自动的,编译器帮你干了),不然就不管它当它不存在.编译器想认识一个函数,那么这个函数就必须实例化,由于每个文件是单独编译的.在func.cpp文件中,编译器一看.嗯......这是一个模板函数的实现但这个模板并没有被谁用到,那么我就不要它了,然而在func.h文件中的情况是,嗯.....这是一个模板函数的声明,嗯......实现不在这里,嗯.......可能在其他地方,待会儿再看看.所以,你一到使用这个函数的时候,编译器就会说,这个模板函数只有声明,没有实现,无法解析.(注意:我上面的解释并不专业,我只是往理解性的方向说,因为要说明白什么是模板实例化,原理是什么,什么是链接器,它是干什么的,感觉又要好几篇才能说清楚,感兴趣的话,可以自己去了解了解.)
那么怎么办呢?
最有效的办法就是,声明实现放一起.(称为包含模型)
但是,你可能会感觉到十分不舒服.因为,你可能更像让它接近面向对象编程的那种感觉.
那么我目前建议你这么做:
//头文件func.h
#ifndef __FUNC_H__
#define __FUNC_H__
//头文件里的函数声明
template<typename T>
void func(T const&);
#define __FUNC_CPP__
#include "func.cpp"
#endif // !__FUNC
//实现文件func.cpp
#ifdef __FUNC_CPP__
#include <iostream>
template <typename T>
void func(T const&) {
std::cout << "我什么也不想做" << std::endl;
}
#endif
当然,这么做并不是那么的完美,如果你自己去打一遍你就会发现显而易见的缺点(没有代码提示).这里我也不想去解释各种预处理器指令的作用了,因为这不是我这篇文章的重点.