-
2018-10-31 14:02:34
本文通过c++ primer plus中的例子来学习写模板类.
1.为什么需要模板类?
为了满足代码重用的需求.
比如stack类,希望不同的类型都能用.
先看看用typedef 定义的stack类.// stack.h -- class definition for the stack ADT #ifndef STACK_H_ #define STACK_H_ typedef unsigned long Item; class Stack { private: enum {MAX = 10}; // constant specific to class Item items[MAX]; // holds stack items int top; // index for top stack item public: Stack(); bool isempty() const; bool isfull() const; // push() returns false if stack already is full, true otherwise bool push(const Item & item); // add item to stack // pop() returns false if stack already is empty, true otherwise bool pop(Item & item); // pop top into item }; Stack::Stack() // create an empty stack { top = 0; } bool Stack::isempty() const { return top == 0; } bool Stack::isfull() const { return top == MAX; } bool Stack::push(const Item & item) { if (top < MAX) { items[top++] = item; return true; } else return false; } bool Stack::pop(Item & item) { if (top > 0) { item = items[--top]; return true; } else return false; } #endif
此时能操作的类型是unsigned long;现在如果希望stack还能够操作string,该怎么办呢?
如果还用typedef定义,就要多一份类似的代码.
这个时候模板类就发挥作用了.2.改写为模板类
使用模板类改写stack如下
// stacktp.h -- a stack template #ifndef STACKTP_H_ #define STACKTP_H_ template <class Type> class Stack { private: enum {MAX = 10}; // constant specific to class Type items[MAX]; // holds stack items int top; // index for top stack item public: Stack(); bool isempty(); bool isfull(); bool push(const Type & item); // add item to stack bool pop(Type & item); // pop top into item }; template <class Type> Stack<Type>::Stack() { top = 0; } template <class Type> bool Stack<Type>::isempty() { return top == 0; } template <class Type> bool Stack<Type>::isfull() { return top == MAX; } template <class Type> bool Stack<Type>::push(const Type & item) { if (top < MAX) { items[top++] = item; return true; } else return false; } template <class Type> bool Stack<Type>::pop(Type & item) { if (top > 0) { item = items[--top]; return true; } else return false; } #endif
(1)将typedef替换为
template <class Type>
,关键字template告诉编译器要定义一个模板,尖括号内容相当于函数的参数列表.class相当于变量的类型名,Type相当于变量的值,变量接受类型作为其值.
(2)使用泛型名Type替换标识符Item,Type是一个通用类型说明符,在使用模板是将使用实际类型替代它.
(3)在每个函数前面使用模板声明template <class Type>
打头.并且在类名后面要<Type>
对比stack构造函数,未使用模板类前,Stack::Stack() // create an empty stack { top = 0; }
使用模板类后
template <class Type> Stack<Type>::Stack() { top = 0; }
3.使用
使用的时候给类传递类型名就可以了
Stack<int> kernels; Stack<string> colonels;
4.模板中的非类型参数使用
经常看到这样的模板定义,
template <class T, int n>
int n代表什么呢?此时该如何使用呢
int n指出n的类型是int,这种类型称为非类型参数或者表达式参数.
template <class T, int n>
class ArrayTP
以下的ArrayTP类如果按照
ArrayTP<double,12> eggWeight的方式调用,表示用double替换T,12替换n.//arraytp.h -- Array Template #ifndef ARRAYTP_H_ #define ARRAYTP_H_ #include <iostream> #include <cstdlib> template <class T, int n> class ArrayTP { private: T ar[n]; public: ArrayTP() {}; explicit ArrayTP(const T & v); virtual T & operator[](int i); virtual T operator[](int i) const; }; template <class T, int n> ArrayTP<T,n>::ArrayTP(const T & v) { for (int i = 0; i < n; i++) ar[i] = v; } template <class T, int n> T & ArrayTP<T,n>::operator[](int i) { if (i < 0 || i >= n) { std::cerr << "Error in array limits: " << i << " is out of range\n"; std::exit(EXIT_FAILURE); } return ar[i]; } template <class T, int n> T ArrayTP<T,n>::operator[](int i) const { if (i < 0 || i >= n) { std::cerr << "Error in array limits: " << i << " is out of range\n"; std::exit(EXIT_FAILURE); } return ar[i]; } #endif
更多相关内容 -
模板类
2018-09-05 10:55:31C++通过类模板来实现泛型支持。 1 基础的类模板 类模板,可以定义相同的操作,拥有不同数据类型的成员属性。 通常使用template来声明。告诉编译器,碰到T不要报错,表示一种泛型. 如下,声明一个普通的类模板: ...C++中有一个重要特性,那就是模板类型。类似于Objective-C中的泛型。C++通过类模板来实现泛型支持。
1 基础的类模板
类模板,可以定义相同的操作,拥有不同数据类型的成员属性。
通常使用
template
来声明。告诉编译器,碰到T
不要报错,表示一种泛型.如下,声明一个普通的类模板:
template <typename T> class Complex{ public: //构造函数 Complex(T a, T b) { this->a = a; this->b = b; } //运算符重载 Complex<T> operator+(Complex &c) { Complex<T> tmp(this->a+c.a, this->b+c.b); return tmp; } private: T a; T b; } int main() { //对象的定义,必须声明模板类型,因为要分配内容 Complex<int> a(10,20); Complex<int> b(20,30); Complex<int> c = a + b; return 0; }
2 模板类的继承
在模板类的继承中,需要注意以下两点:
- 如果父类自定义了构造函数,记得子类要使用构造函数列表来初始化
- 继承的时候,如果子类不是模板类,则必须指明当前的父类的类型,因为要分配内存空间
- 继承的时候,如果子类是模板类,要么指定父类的类型,要么用子类的泛型来指定父类
template <typename T> class Parent{ public: Parent(T p) { this->p = p; } private: T p; }; //如果子类不是模板类,需要指明父类的具体类型 class ChildOne:public Parent<int>{ public: ChildOne(int a,int b):Parent(b) { this->cone = a; } private: int cone; }; //如果子类是模板类,可以用子类的泛型来表示父类 template <typename T> class ChildTwo:public Parent<T>{ public: ChildTwo(T a, T b):Parent<T>(b) { this->ctwo = a; } private: T ctwo; };
3 内部声明定义普通模板函数和友元模板函数
普通模板函数和友元模板函数,声明和定义都写在类的内部,也不会有什么报错。正常。
template <typename T> class Complex { //友元函数实现运算符重载 friend ostream& operator<<(ostream &out, Complex &c) { out<<c.a << " + " << c.b << "i"; return out; } public: Complex(T a, T b) { this->a = a; this->b = b; } //运算符重载+ Complex operator+(Complex &c) { Complex temp(this->a + c.a, this->b + c.b); return temp; } //普通加法函数 Complex myAdd(Complex &c1, Complex &c2) { Complex temp(c1.a + c2.a, c1.b + c2.b); return temp; } private: T a; T b; }; int main() { Complex<int> c1(1,2); Complex<int> c2(3,4); Complex<int> c = c1 + c2; cout<<c<<endl; return 0; }
4 内部声明友元模板函数+外部定义友元模板函数
如果普通的模板函数声明在内的内部,定义在类的外部,不管是否处于同一个文件,就跟普通的函数一样,不会出现任何错误提示。但是如果是友元函数就会出现报错,是因为有
二次编译
这个机制存在。4.1 模板类和模板函数的机制
在编译器进行编译的时候,编译器会产生类的模板函数的声明,当时实际确认类型后调用的时候,会根据调用的类型进行再次帮我们生成对应类型的函数声明和定义。我们称之为
二次编译
。同样,因为这个机制,会经常报错找不到类的函数的实现
。在模板类的友元函数外部定义时,也会出现这个错误。解决方法是 “ 类的前置声明和函数的前置声明 ”。按照普通模板函数的样式处理友元函数
#include <iostream> using namespace std; template <typename T> class Complex { //友元函数实现运算符重载 friend ostream& operator<<(ostream &out, Complex<T> &c); public: Complex(T a, T b); //运算符重载+ Complex<T> operator+(Complex<T> &c); //普通加法函数 Complex<T> myAdd(Complex<T> &c1, Complex<T> &c2); private: T a; T b; }; //友元函数的实现 template <typename T> ostream& operator<<(ostream &out, Complex<T> &c) { out<<c.a << " + " << c.b << "i"; return out; } //函数的实现 template <typename T> Complex<T>::Complex(T a, T b) { this->a = a; this->b = b; } template <typename T> Complex<T> Complex<T>::operator+(Complex<T> &c) { Complex temp(this->a + c.a, this->b + c.b); return temp; } template <typename T> Complex<T> Complex<T>::myAdd(Complex<T> &c1, Complex<T> &c2) { Complex temp(c1.a + c2.a, c1.b + c2.b); return temp; } int main() { Complex<int> c1(1,2); Complex<int> c2(3,4); Complex<int> c = c1 + c2; cout<<c<<endl; return 0; }
友元函数的定义写在类的外部--错误信息
Undefined symbols for architecture x86_64: "operator<<(std::__1::basic_ostream<char, std::__1::char_traits<char> >&, Complex<int>&)", referenced from: _main in demo1.o ld: symbol(s) not found for architecture x86_64 clang: error: linker command failed with exit code 1 (use -v to see invocation)
上面的错误信息,就是典型的二次编译的错误信息,找不到友元函数的函数实现。所以,如果友元模板函数的定义写在函数的外部,需要进行类和函数的前置声明,来让编译器找到函数的实现
4.2 前置声明解决二次编译问题
- 类的前置声明
- 友元模板函数的前置声明
- 友元模板函数声明需要增加泛型支持
前置声明.png
5 声明和定义分别在不同的文件(模板函数、模板友元)
类的声明和实现,分别在不同的文件下,需要增加一个hpp文件支持。或者尽量将模板函数与模板友元放在一个文件下。
- 类的声明与函数的声明写在.h文件
- 类的实现及函数的实现写在.cpp文件
- 将.cpp文件改成.hpp文件
- 在主函数中调用.hpp文件,而不是引用.h文件
如果碰到.h和.hpp文件都存在的情况下,引用.hpp文件。
demo2.h文件
存放类的声明和函数的声明
#include <iostream> using namespace std; //类的前置声明 template <typename T> class Complex; //友元函数的声明 template <typename T> ostream& operator<<(ostream &out, Complex<T> &c); template <typename T> class Complex { //友元函数实现运算符重载 friend ostream& operator<< <T> (ostream &out, Complex<T> &c); public: Complex(T a, T b); //运算符重载+ Complex<T> operator+(Complex<T> &c); //普通加法函数 Complex<T> myAdd(Complex<T> &c1, Complex<T> &c2); private: T a; T b; };
demo2.hpp文件
包括模板函数的实现
#include "demo2.h" //友元函数的实现 template <typename T> ostream& operator<<(ostream &out, Complex<T> &c) { out<<c.a << " + " << c.b << "i"; return out; } //函数的实现 template <typename T> Complex<T>::Complex(T a, T b) { this->a = a; this->b = b; } template <typename T> Complex<T> Complex<T>::operator+(Complex<T> &c) { Complex temp(this->a + c.a, this->b + c.b); return temp; } template <typename T> Complex<T> Complex<T>::myAdd(Complex<T> &c1, Complex<T> &c2) { Complex temp(c1.a + c2.a, c1.b + c2.b); return temp; }
main.cpp文件
需要调用hpp文件
#include <iostream> using namespace std; #include "demo2.hpp" int main() { Complex<int> c1(1,2); Complex<int> c2(3,4); Complex<int> c = c1 + c2; cout<<c<<endl; return 0; }
作者:一月二十三
链接:https://www.jianshu.com/p/70ca94872418
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。 -
类模板和模板类
2019-07-04 21:10:42类模板和模板类 所谓类模板,实际上是建立一个通用类,其数据成员、成员函数的返回值类型和形参类型不具体指定,用一个虚拟的类型来代表。使用类模板定义对象时,系统会实参的类型来取代类模板中虚拟类型从而实现了...类模板和模板类
所谓类模板,实际上是建立一个通用类,其数据成员、成员函数的返回值类型和形参类型不具体指定,用一个虚拟的类型来代表。使用类模板定义对象时,系统会实参的类型来取代类模板中虚拟类型从而实现了不同类的功能。
定义一个类模板与定义函数模板的格式类似,必须以关键字template开始,后面是尖括号括起来的模板参数,然后是类名,其格式如下:
template <typename 类型参数> class 类名{ 类成员声明 }; 或者 template <class 类型参数> class 类名{ 类成员声明 };
-
template:是一个声明模板的关键字,它表明声明一个模板
-
类型参数:通常用C++标识符表示,如T、Type等,实际上是一个虚拟的类型名,现在未指定它是哪一种具体的类型,但使用类模板时,必须将类型参数实例化。
-
typename和class的作用相同,都是表示其后面的参数是一个虚拟的类名(即类型参数).
在类声明中,欲采用通用数据类型的数据成员、成员函数的参数或返回类型前面需要加上类型参数。
如建立一个用来实现求两个数最大值的类模板
template<typename T> //模板声明,其中T为类型参数 class Compare{ public: Compare(T i,T j) { x = i; y = j; } T max() { return (x>y)?x:y; } private: T x,y; };
用类模板定义对象时,采用以下形式:
类模板名<实际类型名>对象名[(实参表列)];
因此,使用上面求最大值的类型模板的主函数可写成:
int main() { Compare<int>com1(3,7); Compare<double>com2(12.34,56.78); Compare<char>com3('a','x'); cout<<"其中的最大值是:"<<com1.max()<<endl; cout<<"其中的最大值是:"<<com2.max()<<endl; cout<<"其中的最大值是:"<<com3.max()<<endl; return 0; }
例6.6 类模板compare的使用举例
#include<iostream.h> template<typename T> //模板声明,其中T为类型参数 class Compare{ public: Compare(T i,T j) { x = i; y = j; } T max() { return (x>y)?x:y; } private: T x,y; }; int main() { Compare<int>com1(3,7); //用类模板定义对象com1,此时T被int替代 Compare<double>com2(12.34,56.78); //用类模板定义对象com2,此时T被double替代 Compare<char>com3('a','x'); //用类模板定义对象com3,此时T被char替代 cout<<"其中的最大值是:"<<com1.max()<<endl; cout<<"其中的最大值是:"<<com2.max()<<endl; cout<<"其中的最大值是:"<<com3.max()<<endl; return 0; }
程序运行结果是:
其中的最大值是:7 其中的最大值是:56.78 其中的最大值是:x
在以上例子中,成员函数(其中含有类型参数)是定义类体内的。但是,类模板中的成员函数,也可以在类模板外定义。此时,若成员函数中有参数类型存在,则C++有一些特殊的规定:
(1)需要在成员函数定义之前进行模板声明; (2)在成员函数名前缀上"类名<类型参数>::";
在类模板外定义成员函数的一般形式如下:
temlate<typename 类型参数> 函数类型 类名<类型参数>::成员函数名(形参表) { 函数体; } 如上题中成员函数max在类模板外定义时,应该写成: template<typename T> T Compare<T>::max() { return (x>y)?x:y; }
//例6.7 在类模板外定义成员函数函数举例。
#include<iostream.h> template<typename T> //模板声明,其中T为类型参数 class Compare{ public: Compare(T i,T j) { x = i; y = j; } T max(); private: T x,y; }; template<class T> T Compare<T>::max() { return (x>y)?x:y; } int main() { Compare<int>com1(3,7); //用类模板定义对象com1,此时T被int替代 Compare<double>com2(12.34,56.78); //用类模板定义对象com2,此时T被double替代 Compare<char>com3('a','x'); //用类模板定义对象com3,此时T被char替代 cout<<"其中的最大值是:"<<com1.max()<<endl; cout<<"其中的最大值是:"<<com2.max()<<endl; cout<<"其中的最大值是:"<<com3.max()<<endl; return 0; }
/*
程序运行结果是:其中的最大值是:7 其中的最大值是:56.78 其中的最大值是:x
此例中,类模板Compare经实例化后生成了3个类型分别为int、double、char的模板类,这3个模板类
经实例化后又生成了3个对象com1、com2、com3。类模板代表了一类类,模板类表示某一具体的类。关系如下:类模板 Compare<T> 实例化成模板类:Compare<int> Compare<double> Compare<char> 实例化模板类对象:com1 com2 com3
例6.8 类模板Stack的使用举例。
#include<iostream.h> const int size=10; template<class T> //模板声明,其中T为类型参数 class Stack{ //类模板为Stack public: void init() { tos=0; } void push(T ob); //声明成员函数push的原型,函数参数类型为T类型 T pop(); //声明成员函数pop的原型,其返回值类型为T类型 private: T stack[size]; //数组类型为T,即是自可取任意类型 int tos; }; template<class T> //模板声明 void Stack<T>::push(T ob) //在类模板体外定义成员函数push { if(tos==size) { cout<<"Stack is full"<<endl; return; } stack[tos]=ob; tos++; } template<typename T> //模板声明 T Stack<T>::pop() //在类模板体外定义成员函数push { if(tos==0) { cout<<"Stack is empty"<<endl; return 0; } tos--; return stack[tos]; } int main() { //定义字符堆栈 Stack<char> s1; //用类模板定义对象s,此时T被char取代 s1.init(); s1.push('a'); s1.push('b'); s1.push('c'); for(int i=0;i<3;i++){cout<<"pop s1:"<<s1.pop()<<endl;} //定义整型堆栈 Stack<int> s2; //用类模板定义对象s,此时T被int取代 s2.init(); s2.push(1); s2.push(3); s2.push(5); for(int i=0;i<3;i++){cout<<"pop s2:"<<s2.pop()<<endl;} return 0; }
/*
程序运行结果是:pop s1:c pop s1:b pop s1:a pop s2:5 pop s2:3 pop s2:1
说明:
-
在每一个类模板定义之前,都需要在前面加上模板声明,如
template
或
template
并且,类模板在使用时,必须在模板类名字后面缀上<类型参数> ,如
Stack -
如同模板函数一样,模板类也可以有多个类型参数。
例6.9 有两个类型参数的类模板举例
#include<iostream.h> template<class QQ,class T> //声明模板,具有T1,T2两个类型参数 class Myclass{ //定义模板类Myclass public: Myclass(QQ a,T b); void show(); private: QQ x; T y; }; template<typename QQ,typename T> Myclass<QQ,T>::Myclass(QQ a,T b) { x = a; y = b; } template<class QQ,class T> void Myclass<QQ,T>::show() { cout<<"x="<<x<<","<<"y="<<y<<endl; } int main() { Myclass <int,double>m1(12,0.15); //用类模板定义对象m1,此时T1,T2分别被int、double取代 Myclass <int,char*>m2(12,"This a test."); //用类模板定义对象m2,此时T1,T2分别被int,char*取代 m1.show(); m2.show(); return 0; } /* 程序运行结果是: x=12,y=0.15 x=12,y=This a test. */
程序猿神奇的手,每时每刻,这双手都在改变着世界的交互方式!
更多C++相关知识体系,请移步C++知识目录。 -
-
C++模板类
2018-11-19 12:14:44看看我们几乎每天都能遇到的模板类吧,如STL, ATL, WTL, 以及Boost的模板类,都能体会到这样的滋味:接口简单,操作复杂。 我在5年前开始使用模板,那时我看到了MFC的容器类。直到去年我还没有必要自己编写模板类。...理解编译器的编译模板过程
如何组织编写模板程序
前言
常遇到询问使用模板到底是否容易的问题,我的回答是:“模板的使用是容易的,但组织编写却不容易”。看看我们几乎每天都能遇到的模板类吧,如STL, ATL, WTL, 以及Boost的模板类,都能体会到这样的滋味:接口简单,操作复杂。我在5年前开始使用模板,那时我看到了MFC的容器类。直到去年我还没有必要自己编写模板类。可是在我需要自己编写模板类时,我首先遇到的事实却是“传统”编程方法(在*.h文件声明,在*.cpp文件中定义)不能用于模板。于是我花费一些时间来了解问题所在及其解决方法。
本文对象是那些熟悉模板但还没有很多编写模板经验的程序员。本文只涉及模板类,未涉及模板函数。但论述的原则对于二者是一样的。
问题的产生
通过下例来说明问题。例如在array.h文件中有模板类array:
// array.h
template <typename T, int SIZE>
class array
{
T data_[SIZE];
array (const array& other);
const array& operator = (const array& other);
public:
array(){};
T& operator[](int i) {return data_[i];}
const T& get_elem (int i) const {return data_[i];}
void set_elem(int i, const T& value) {data_[i] = value;}
operator T*() {return data_;}
};然后在main.cpp文件中的主函数中使用上述模板:
// main.cpp
#include “array.h”int main(void)
{
array<int, 50> intArray;
intArray.set_elem(0, 2);
int firstElem = intArray.get_elem(0);
int* begin = intArray;
}这时编译和运行都是正常的。程序先创建一个含有50个整数的数组,然后设置数组的第一个元素值为2,再读取第一个元素值,最后将指针指向数组起点。
但如果用传统编程方式来编写会发生什么事呢?我们来看看:
将array.h文件分裂成为array.h和array.cpp二个文件(main.cpp保持不变)
// array.h
template <typename T, int SIZE>
class array
{
T data_[SIZE];
array (const array& other);
const array& operator = (const array& other);
public:
array(){};
T& operator[](int i);
const T& get_elem (int i) const;
void set_elem(int i, const T& value);
operator T*();
};// array.cpp
#include “array.h”template<typename T, int SIZE> T& array<T, SIZE>::operator [](int i)
{
return data_[i];
}template<typename T, int SIZE> const T& array<T, SIZE>::get_elem(int i) const
{
return data_[i];
}template<typename T, int SIZE> void array<T, SIZE>::set_elem(int i, const T& value)
{
data_[i] = value;
}
template<typename T, int SIZE> array<T, SIZE>::operator T*()
{
return data_;
}编译时会出现3个错误。问题出来了:
为什么错误都出现在第一个地方?
为什么只有3个链接出错?array.cpp中有4个成员函数。要回答上面的问题,就要深入了解模板的实例化过程。
模板实例化
程序员在使用模板类时最常犯的错误是将模板类视为某种数据类型。所谓类型参量化(parameterized types)这样的术语导致了这种误解。模板当然不是数据类型,模板就是模板,恰如其名:编译器使用模板,通过更换模板参数来创建数据类型。这个过程就是模板实例化(Instantiation)。
从模板类创建得到的类型称之为特例(specialization)。
模板实例化取决于编译器能够找到可用代码来创建特例(称之为实例化要素,
point of instantiation)。
要创建特例,编译器不但要看到模板的声明,还要看到模板的定义。
模板实例化过程是迟钝的,即只能用函数的定义来实现实例化。再回头看上面的例子,可以知道array是一个模板,array<int, 50>是一个模板实例 - 一个类型。从array创建array<int, 50>的过程就是实例化过程。实例化要素体现在main.cpp文件中。如果按照传统方式,编译器在array.h文件中看到了模板的声明,但没有模板的定义,这样编译器就不能创建类型array<int, 50>。但这时并不出错,因为编译器认为模板定义在其它文件中,就把问题留给链接程序处理。
现在,编译array.cpp时会发生什么问题呢?编译器可以解析模板定义并检查语法,但不能生成成员函数的代码。它无法生成代码,因为要生成代码,需要知道模板参数,即需要一个类型,而不是模板本身。
这样,链接程序在main.cpp 或 array.cpp中都找不到array<int, 50>的定义,于是报出无定义成员的错误。
至此,我们回答了第一个问题。但还有第二个问题,在array.cpp中有4个成员函数,链接器为什么只报了3个错误?回答是:实例化的惰性导致这种现象。在main.cpp中还没有用上operator[],编译器还没有实例化它的定义。
解决方法
认识了问题,就能够解决问题:
在实例化要素中让编译器看到模板定义。
用另外的文件来显式地实例化类型,这样链接器就能看到该类型。
使用export关键字。前二种方法通常称为包含模式,第三种方法则称为分离模式。
第一种方法意味着在使用模板的转换文件中不但要包含模板声明文件,还要包含模板定义文件。在上例中,就是第一个示例,在array.h中用行内函数定义了所有的成员函数。或者在main.cpp文件中也包含进array.cpp文件。这样编译器就能看到模板的声明和定义,并由此生成array<int, 50>实例。这样做的缺点是编译文件会变得很大,显然要降低编译和链接速度。
第二种方法,通过显式的模板实例化得到类型。最好将所有的显式实例化过程安放在另外的文件中。在本例中,可以创建一个新文件templateinstantiations.cpp:
// templateinstantiations.cpp
#include “array.cpp”template class array <int, 50>; // 显式实例化
array<int, 50>类型不是在main.cpp中产生,而是在templateinstantiations.cpp中产生。这样链接器就能够找到它的定义。用这种方法,不会产生巨大的头文件,加快编译速度。而且头文件本身也显得更加“干净”和更具有可读性。但这个方法不能得到惰性实例化的好处,即它将显式地生成所有的成员函数。另外还要维护templateinstantiations.cpp文件。
第三种方法是在模板定义中使用export关键字,剩下的事就让编译器去自行处理了。当我在
Stroustrup的书中读到export时,感到非常兴奋。但很快就发现VC 6.0不支持它,后来又发现根本没有编译器能够支持这个关键字(第一个支持它的编译器要在2002年底才问世)。自那以后,我阅读了不少关于export的文章,了解到它几乎不能解决用包含模式能够解决的问题。欲知更多的export关键字,建议读读Herb Sutter撰写的文章。结论
要开发模板库,就要知道模板类不是所谓的"原始类型",要用其它的编程思路。本文目的不是要吓唬那些想进行模板编程的程序员。恰恰相反,是要提醒他们避免犯下开始模板编程时都会出现的错误。//
http://www.cnblogs.com/xgchang/archive/2004/11/12/63139.aspx
甚至是在定义非内联函数时,模板的头文件中也会放置所有的声明和定义。这似乎违背了通常的头文件规则:“不要在分配存储空间前放置任何东西”,这条规则是为了防止在连接时的多重定义错误。但模板定义很特殊。由template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直出于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉模板的多重定义,所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。为什么C++编译器不能支持对模板的分离式编译
刘未鹏(pongba) /文首先,C++标准中提到,一个编译单元[translation unit]是指一个.cpp文件以及它所include的所有.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.obj文件,后者拥有PE[Portable Executable,即windows可执行文件]文件格式,并且本身包含的就已经是二进制码,但是,不一定能够执行,因为并不保证其中一定有main函数。当编译器将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由连接器(linker)进行连接成为一个.exe文件。
举个例子:
//---------------test.h-------------------//
void f();//这里声明一个函数f
//---------------test.cpp--------------//
#include”test.h”
void f()
{
…//do something
} //这里实现出test.h中声明的f函数
//---------------main.cpp--------------//
#include”test.h”
int main()
{
f(); //调用f,f具有外部连接类型
}
在这个例子中,test. cpp和main.cpp各被编译成为不同的.obj文件[姑且命名为test.obj和main.obj],在main.cpp中,调用了f函数,然而当编译器编译main.cpp时,它所仅仅知道的只是main.cpp中所包含的test.h文件中的一个关于void f();的声明,所以,编译器将这里的f看作外部连接类型,即认为它的函数实现代码在另一个.obj文件中,本例也就是test.obj,也就是说,main.obj中实际没有关于f函数的哪怕一行二进制代码,而这些代码实际存在于test.cpp所编译成的test.obj中。在main.obj中对f的调用只会生成一行call指令,像这样:
call f [C++中这个名字当然是经过mangling[处理]过的]
在编译时,这个call指令显然是错误的,因为main.obj中并无一行f的实现代码。那怎么办呢?这就是连接器的任务,连接器负责在其它的.obj中[本例为test.obj]寻找f的实现代码,找到以后将call f这个指令的调用地址换成实际的f的函数进入点地址。需要注意的是:连接器实际上将工程里的.obj“连接”成了一个.exe文件,而它最关键的任务就是上面说的,寻找一个外部连接符号在另一个.obj中的地址,然后替换原来的“虚假”地址。
这个过程如果说的更深入就是:
call f这行指令其实并不是这样的,它实际上是所谓的stub,也就是一个
jmp 0x23423[这个地址可能是任意的,然而关键是这个地址上有一行指令来进行真正的call f动作。也就是说,这个.obj文件里面所有对f的调用都jmp向同一个地址,在后者那儿才真正”call”f。这样做的好处就是连接器修改地址时只要对后者的call XXX地址作改动就行了。但是,连接器是如何找到f的实际地址的呢[在本例中这处于test.obj中],因为.obj于.exe的格式都是一样的,在这样的文件中有一个符号导入表和符号导出表[import table和export table]其中将所有符号和它们的地址关联起来。这样连接器只要在test.obj的符号导出表中寻找符号f[当然C++对f作了mangling]的地址就行了,然后作一些偏移量处理后[因为是将两个.obj文件合并,当然地址会有一定的偏移,这个连接器清楚]写入main.obj中的符号导入表中f所占有的那一项。
这就是大概的过程。其中关键就是:
编译main.cpp时,编译器不知道f的实现,所有当碰到对它的调用时只是给出一个指示,指示连接器应该为它寻找f的实现体。这也就是说main.obj中没有关于f的任何一行二进制代码。
编译test.cpp时,编译器找到了f的实现。于是乎f的实现[二进制代码]出现在test.obj里。
连接时,连接器在test.obj中找到f的实现代码[二进制]的地址[通过符号导出表]。然后将main.obj中悬而未决的call XXX地址改成f实际的地址。
完成。然而,对于模板,你知道,模板函数的代码其实并不能直接编译成二进制代码,其中要有一个“具现化”的过程。举个例子:
//----------main.cpp------//
template
void f(T t)
{}
int main()
{
…//do something
f(10); //call f 编译器在这里决定给f一个f的具现体
…//do other thing
}
也就是说,如果你在main.cpp文件中没有调用过f,f也就得不到具现,从而main.obj中也就没有关于f的任意一行二进制代码!!如果你这样调用了:
f(10); //f得以具现化出来
f(10.0); //f得以具现化出来
这样main.obj中也就有了f,f两个函数的二进制代码段。以此类推。
然而具现化要求编译器知道模板的定义,不是吗?
看下面的例子:[将模板和它的实现分离]
//-------------test.h----------------//
template
class A
{
public:
void f(); //这里只是个声明
};
//---------------test.cpp-------------//
#include”test.h”
template
void A::f() //模板的实现,但注意:不是具现
{
…//do something
}
//---------------main.cpp---------------//
#include”test.h”
int main()
{
A a;
a. f(); //编译器在这里并不知道A::f的定义,因为它不在test.h里面
//于是编译器只好寄希望于连接器,希望它能够在其他.obj里面找到
//A::f的实现体,在本例中就是test.obj,然而,后者中真有A::f的
//二进制代码吗?NO!!!因为C++标准明确表示,当一个模板不被用到的时
//侯它就不该被具现出来,test.cpp中用到了A::f了吗?没有!!所以实
//际上test.cpp编译出来的test.obj文件中关于A::f的一行二进制代码也没有
//于是连接器就傻眼了,只好给出一个连接错误
//但是,如果在test.cpp中写一个函数,其中调用A::f,则编译器会将其//具现出来,因为在这个点上[test.cpp中],编译器知道模板的定义,所以能//够具现化,于是,test.obj的符号导出表中就有了A::f这个符号的地
//址,于是连接器就能够完成任务。
}关键是:在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找[当遇到未决符号时它会寄希望于连接器]。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会具现化出来,所以,当编译器只看到模板的声明时,它不能具现化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的.cpp文件中没有用到模板的具现体时,编译器懒得去具现,所以,整个工程的.obj中就找不到一行模板具现体的二进制代码,于是连接器也黔
/
http://dev.csdn.net/develop/article/19/19587.shtm
C++模板代码的组织方式 ——包含模式(Inclusion Model) 选择自 sam1111 的 Blog
关键字 Template Inclusion Model
出处 C++ Template: The Complete Guide说明:本文译自《C++ Template: The Complete Guide》一书的第6章中的部分内容。最近看到C++论坛上常有关于模板的包含模式的帖子,联想到自己初学模板时,也为类似的问题困惑过,因此翻译此文,希望对初学者有所帮助。
模板代码有几种不同的组织方式,本文介绍其中最流行的一种方式:包含模式。
链接错误
大多数C/C++程序员向下面这样组织他们的非模板代码:
·类和其他类型全部放在头文件中,这些头文件具有.hpp(或者.H, .h, .hh, .hxx)扩展名。 ·对于全局变量和(非内联)函数,只有声明放在头文件中,而定义放在点C文件中,这些文件具有.cpp(或者.C, .c, .cc, .cxx)扩展名。
这种组织方式工作的很好:它使得在编程时可以方便地访问所需的类型定义,并且避免了来自链接器的“变量或函数重复定义”的错误。
由于以上组织方式约定的影响,模板编程新手往往会犯一个同样的错误。下面这一小段程序反映了这种错误。就像对待“普通代码”那样,我们在头文件中定义模板:
// basics/myfirst.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP// declaration of template
template
void print_typeof (T const&);
#endif // MYFIRST_HPP
print_typeof()声明了一个简单的辅助函数用来打印一些类型信息。函数的定义放在点C文件中:
// basics/myfirst.cpp
#include
#include
#include “myfirst.hpp”
// implementation/definition of template
template
void print_typeof (T const& x)
{std::cout << typeid(x).name() << std::endl;
}
这个例子使用typeid操作符来打印一个字符串,这个字符串描述了传入的参数的类型信息。
最后,我们在另外一个点C文件中使用我们的模板,在这个文件中模板声明被#include:
// basics/myfirstmain.cpp
#include “myfirst.hpp”
// use of the template
int main()
{double ice = 3.0; print_typeof(ice); // call function template for type double
}
大部分C++编译器(Compiler)很可能会接受这个程序,没有任何问题,但是链接器(Linker)大概会报告一个错误,指出缺少函数print_typeof()的定义。
这个错误的原因在于,模板函数print_typeof()的定义还没有被具现化(instantiate)。为了具现化一个模板,编译器必须知道哪一个定义应该被具现化,以及使用什么样的模板参数来具现化。不幸的是,在前面的例子中,这两组信息存在于分开编译的不同文件中。因此,当我们的编译器看到对print_typeof()的调用,但是没有看到此函数为double类型具现化的定义时,它只是假设这样的定义在别处提供,并且创建一个那个定义的引用(链接器使用此引用解析)。另一方面,当编译器处理myfirst.cpp时,该文件并没有任何指示表明它必须为它所包含的特殊参数具现化模板定义。
头文件中的模板
解决上面这个问题的通用解法是,采用与我们使用宏或者内联函数相同的方法:我们将模板的定义包含进声明模板的头文件中。对于我们的例子,我们可以通过将#include "myfirst.cpp"添加到myfirst.hpp文件尾部,或者在每一个使用我们的模板的点C文件中包含myfirst.cpp文件,来达到目的。当然,还有第三种方法,就是删掉myfirst.cpp文件,并重写myfirst.hpp文件,使它包含所有的模板声明与定义:
// basics/myfirst2.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP#include
#include// declaration of template
template
void print_typeof (T const&);// implementation/definition of template
template
void print_typeof (T const& x)
{std::cout << typeid(x).name() << std::endl;
}
#endif // MYFIRST_HPP
这种组织模板代码的方式就称作包含模式。经过这样的调整,你会发现我们的程序已经能够正确编译、链接、执行了。
从这个方法中我们可以得到一些观察结果。最值得注意的一点是,这个方法在相当程度上增加了包含myfirst.hpp的开销。在这个例子中,这种开销并不是由模板定义自身的尺寸引起的,而是由这样一个事实引起的,即我们必须包含我们的模板用到的头文件,在这个例子中是和。你会发现这最终导致了成千上万行的代码,因为诸如这样的头文件也包含了和我们类似的模板定义。
这在实践中确实是一个问题,因为它增加了编译器在编译一个实际程序时所需的时间。我们因此会在以后的章节中验证其他一些可能的方法来解决这个问题。但无论如何,现实世界中的程序花一小时来编译链接已经是快的了(我们曾经遇到过花费数天时间来从源码编译的程序)。
抛开编译时间不谈,我们强烈建议如果可能尽量按照包含模式组织模板代码。
另一个观察结果是,非内联模板函数与内联函数和宏的最重要的不同在于:它并不会在调用端展开。相反,当模板函数被具现化时,会产生此函数的一个新的拷贝。由于这是一个自动的过程,编译器也许会在不同的文件中产生两个相同的拷贝,从而引起链接器报告一个错误。理论上,我们并不关心这一点:这是编译器设计者应当关心的事情。实际上,大多数时候一切都运转正常,我们根本就不用处理这种状况。然而,对于那些需要创建自己的库的大型项目,这个问题偶尔会显现出来。
最后,需要指出的是,在我们的例子中,应用于普通模板函数的方法同样适用于模板类的成员函数和静态数据成员,以及模板成员函数。
-
模板类与模板类嵌套
2019-01-11 11:36:53在C++中定义一个模板类,不能正确的定义move函数, template<class T> class List{ }; template< class T> class DoubleLinkList:public List< T > { private: /... -
C++类模板和模板类
2018-11-29 14:32:35C++通过类模板来实现泛型支持。 1 基础的类模板 类模板,可以定义相同的操作,拥有不同数据类型的成员属性。 通常使用template来声明。告诉编译器,碰到T不要报错,表示一种泛型. 如下,声明一个普通的类模板: ... -
C++模板类的运算符重载
2022-03-31 20:13:41有关模板类的流插入运算符重载问题 -
类模板与模板类概念区分
2019-04-27 09:53:37类模板的定义: 允许用户为类定义个一种模式,使得类中的某些数据成员、...模板类: 就是类模板中的参数确定之后的产物,也就是类模板实例化后的产物。(它是一个参数已经确定好的类) A<int> A<cha... -
C++模板类头、源文件分离方法(2)
2021-07-29 10:16:31C++模板类头、源文件分离方法(2) 方法一 按C++ primer中的包含模型,在定义模板类的头文件a.h中的末行添加语句: #include "a.cpp" 在使用模板的测试文件test.cpp中包含头文件即可: #include "a.h" Examples a... -
C++模板类详解及注意事项
2020-08-16 09:11:55C++类模板 C++语言引入模板技术,它使用参数化的类型创建相应的函数和类,分别称之为函数模板和类模板 函数模板: 可以用来创建一个通用功能的函数,以支持多种不同形参,进一步简化重载函数的函数体 请参考:函数... -
Sophus库(Linux下)的安装(模板类.hpp,非模板类.h)
2022-02-01 15:13:19Sophus库(Linux下)的安装(包含模板类.hpp,非模板类.h) -
C++:类模板与模板类的区别
2020-07-08 17:39:35写在前面:(模板类是类模板实例化后的一个产物,类模板比作是一个作饺子的模子,而模板类就是用这个模子做出来的饺子,至于饺子什么馅儿的就需要你自己去实例化自己的内容。) 类模板:说明该类是一个模板,它代表... -
模板类与类模板、函数模板与模板函数等的区别
2019-05-08 10:12:04模板类与类模板、函数模板与模板函数等的区别 函数指针 = 指向函数的指针 指针函数=返回指针的函数 数组指针=指向数组的指针 指针数组=内容是指针的数组 类模板=用来产生类的模板 模板类=使用类模板产生的类... -
C++模板类/函数,将头文件与源文件分离
2020-09-18 11:46:11将包含模板的源文件与头文件分开 关键在于模板显式实例化。 解决方案 模板函数分离源文件 按一般方式编写.h和.cpp文件,但注意添加template <typename T>的声明; 在.cpp文件末尾,添加模板显式实例化代码... -
C++ 模板类链表
2018-10-03 00:52:00本程序使用模板类创建链表,并实现添加数据,删除数据,反转链表,连接两链表功能。 主程序 main.cpp //Written by Xuebi //本程序使用模板类创建链表,并实现添加数据,删除数据,反转链表,连接两链表功能 #... -
c++模板类声明和定义的问题
2019-08-18 21:38:39这里在学习的过程中遇到的一些问题,比较简单,但还是记下来,以免下次遇到这个问题再犯,大佬们可跳过哦。先简单的介绍下模板的概念 C++模板(泛型编程) c++模板是泛型编程的基础,所谓...模板类和类模板的概念 一个... -
Sophus 模板类与非模板类使用区别
2018-12-18 17:01:09使用Sophus模板类时可能存在找不到路径的问题,看到其他博主将CMakeList中的Sophus路径改为绝对路径即可,但测试之后并不可以,解决方法是:将Sophus安装之后就可以使用了。... -
C++ 模板类和友元
2018-08-15 11:07:38模板类的友元分三类: 1,非模板友元。 2,约束模板友元,即友元的类型取决于类被实例化时的类型。 3,非约束模板友元,即友元的所有具体化都是类的每一个具体化的友元。 1,模板类的非模板友元函数 ... -
C++中模板类中的成员函数以及模板函数在类外定义
2020-03-05 18:45:07但是模板类里的成员函数和模板函数与普通的成员函数在类外定义不同。 先定义一个模板类以及成员函数和模板函数: 接下我们就在类外定义函数: 1.构造函数 2.成员函数 3.模板函数 以上就是模板类中的成员函数以及... -
C++模板函数 & 模板类
2017-12-18 16:36:32C++ 模板函数 & 模板类 -
普通类继承模板类
2019-04-13 16:24:48普通类继承模板类,在继承时实例化了模板类,那么在普通类的构造函数中需要初始化模板类的实例。 如下所示 BASE.HPP #include <iostream> template <typename T> class base{ public: base(T v){ ... -
类模板和模板类的定义
2019-05-09 13:03:45https://www.cnblogs.com/cxq0017/p/6076856.html 模板类就是在定义函数中参数类型时 定义一个未知类型 而类模板就是在写类的时候定义几个未知参数类型然后进行应用 谢谢上文中的博主 ... -
C++ 模板类与头文件
2019-08-04 20:12:03今天将模板类函数分成了声明和定义两个文件: 模板类的声明: #pragma once #ifndef FIND_ITEM #define FIND_TIEM template <typename elemType> const elemType* find_item(const elemType* first, ... -
C++ 类模板与模板类详解
2018-03-24 17:22:58转自:https://www.cnblogs.com/cxq0017/p/6076856.html在C++的Template... 事实上class用于定义类,在模板引入c++后,最初定义模板的方法为:template<class T>,这里class关键字表明T是一个类型,后... -
非模板类里面的模板构造函数
2018-08-06 14:10:00它里面有这么一个东西,一个类A,类A本身不是定义的模板类,但是它的构造函数却是用的template声明的模板函数(模板构造函数),之前工作直接接触模板比较少,对于这种语法也是第一次这么明确地看到,记录下来备忘。... -
java的模板类
2018-11-04 22:30:51分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇...分享知识,造福人民,实现我们中华民族伟大复兴! ... java的模板类可以理 -
C++ 模板类的声明与实现分离问题(模板实例化)
2018-10-25 11:11:03之类的),头文件不能与cpp文件分离。这就意味者,你头文件定义的含模版的地方必须在头文件中实现,没用模版定义的地方可以放在cpp中实现。 否则,将产生错误信息: 严重性 代码 说明 项目 文件 行 禁止显示状态 ... -
C++模板类声明和定义几种写法
2018-11-29 01:01:17为什么模板类的实现放在cpp会出错 在编译用到了模板类的编译单元时,编译器需要访问方法或者类的实现,以实例化它们。 如果这些实现不在头文件中,则它们将不可访问,因此编译器将无法实例化模板,进而会导致编译... -
C++模板类的使用以及运算符重载的实现
2018-07-04 17:02:26类模板是对一批仅仅成员数据类型不同的类的抽象,程序员只要为这一批类所组成的整个类家族创建一个类模板,给出一套程序代码,就可以用来生成多种具体的类,(这类可以看作是类模板的实例),从而大大提高编程的效率。... -
C++类模板类成员的类外定义方法
2019-09-05 16:18:23//代表这是个类模板 class BinaryTree { public: BinaryTree(); ~BinaryTree(); T PreBTreeSearch(); T InBTreeSearch(T) { //类模板内成员定义 } private: }; /*类模板的构造函...