c++模板函數聲明定義分離編譯錯誤詳解


今天看到accelerated c++上有個簡單的vector容器的實現Vec,就再vs2008上編譯了下:

 

/////  Vec.h

#ifndef GUARD_VEC_H
#define GUARD_VEC_H

#include <iostream>
#include <iterator>
#include <memory>
//#include <xmemory>

template <class T>
class Vec
{
public:
	typedef T* iterator;
	typedef const T* const_iterator;
	typedef size_t size_type;
	typedef T value_type;
	typedef T& reference;
	typedef const T& const_reference;

	Vec() {create();} //默認的構造函數
	explicit Vec(size_type n,const T& t=t()) {create(n,t);} //單參數或者兩個參數構造函數
	Vec(const Vec& v) {create(v.begin(),v.end());} //拷貝構造函數
	Vec& operator=(const Vec&);  //賦值構造函數
	~Vec() {uncreate();} //析構函數
	
	size_type size() { return avail-data; } //定義類的大小,ptrdiff_t自動轉化成size_t
	void push_back(const T& t)
	{
		if (avail==limit)
		{
			grow();
		}
		unchecked_append(t);
	}
	//重載【】
	T& operator[] (size_type i) { return data[i]; }
	const T& operator[] (size_type i) const { return data[i]; }
	//定義begin和end,都有兩個版本
	iterator begin() {return data;}
	const_iterator begin() const {return data;}
	iterator end() {return avail;}
	const_iterator end() const {return avail;}
protected:
private:
	iterator data; //Vec中得初始值
	iterator avail; //Vec中得結束值
	iterator limit; //Vec中空間分配的結束值
	std::allocator<T> alloc; //注意此處std
	//創造函數,負責內存管理
	void create();
	void create(size_type,const T&);
	void create(const_iterator,const_iterator);
	//銷毀元素,返回內存
	void uncreate();
	//支持push_back函數
	void grow();
	void unchecked_append(const T&);
};

#endif

 

////  Vec.cpp

#include <iostream>
#include "Vec.h"
//#pragma comment(lib,"ws2_32.lib")

using namespace std;

//拷貝構造函數
template <class T>
Vec<T>& Vec<T>::operator=(const Vec& v)
{
	if (&v!=this) //檢查是否為自我賦值,很重要,必須有
	{
		uncreate(); //清空左值的元素
		create(v.begin(),v.end()); //拷貝元素到左值
	}
	return *this;
}



//push_back函數中內存增長策略函數
template <class T>
void Vec<T>::grow()
{
	size_type new_size=max(2*(limit-data),ptrdiff_t(1)); //防止剛開始內存空間為0的情況
	iterator new_data=alloc.allocate(new_size); //返回首地址
	//把前兩個參數指定的元素復制給第三個參數表示的目標序列,返回末尾元素的下一個迭代器
	iterator new_avail=uninitialized_copy(data,avail,new_data); 
	uncreate(); //釋放原先的空間

	data=new_data;
	avail=new_avail;
	limit=data+new_size;
}

//向申請的內存中添加元素
template <class T>
void Vec<T>::unchecked_append(const T& val)
{
	//在未初始化的空間構建一個對象,參數1插入對象的位置指針,參數2需要添加的對象
	alloc.construct(avail++,val); 
}


//申請內存的函數create
template <class T>
void Vec<T>::create()
{
	data=avail=limit=0;
}
template <class T>
void Vec<T>::create(size_type n,const T& val)
{
	data=alloc.allocate(n); //申請內存空間,但是不初始化
	limit=avail=data+n;
	uninitialized_fill(data,limit,val); //進行初始化
}
template <class T>
void Vec<T>::create(const_iterator i,const_iterator j)
{
	data=alloc.allocate(j-i);
	limit=avail=uninitialized_copy(i,j,data);
}

//回收內存
template <class T>
void Vec<T>::uncreate()
{
	if (data) //如果data是0,我們不需要做什么工作
	{
		iterator it=avail;
		while (it!=data)
			alloc.destroy(--it); //銷毀沒個元素,為了與delete行為一致,采用從后向前遍歷
		alloc.deallocate(data,limit-data); //內存釋放,函數需要一個非零指針
		                                   //因此,檢測data是否為零
	}
	data=limit=avail=0;
}

 

//// 測試的main函數

#include <iostream>
#include "Vec.h"
using namespace std;
int main()
{
	Vec<int> a;
	Vec<int> b;
	a.push_back(12);
	b=a;
	return 0;
}

 

結果編譯后出現下面錯誤:

1>------ 已啟動生成: 項目: Accelerated, 配置: Debug Win32 ------
1>正在編譯...
1>Vec.cpp
1>Vec_example.cpp
1>正在生成代碼...
1>正在鏈接...
1>Vec_example.obj : error LNK2001: 無法解析的外部符號 "public: class Vec<int> & __thiscall Vec<int>::operator=(class Vec<int> const &)" (??4?$Vec@H@@QAEAAV0@ABV0@@Z)
1>Vec_example.obj : error LNK2001: 無法解析的外部符號 "private: void __thiscall Vec<int>::create(void)" (?create@?$Vec@H@@AAEXXZ)
1>Vec_example.obj : error LNK2001: 無法解析的外部符號 "private: void __thiscall Vec<int>::uncreate(void)" (?uncreate@?$Vec@H@@AAEXXZ)
1>Vec_example.obj : error LNK2001: 無法解析的外部符號 "private: void __thiscall Vec<int>::unchecked_append(int const &)" (?unchecked_append@?$Vec@H@@AAEXABH@Z)
1>Vec_example.obj : error LNK2001: 無法解析的外部符號 "private: void __thiscall Vec<int>::grow(void)" (?grow@?$Vec@H@@AAEXXZ)
1>E:\360data\重要數據\我的文檔\Visual Studio 2008\Projects\Accelerated\Debug\Accelerated.exe : fatal error LNK1120: 5 個無法解析的外部命令
1>生成日志保存在“file://e:\360data\重要數據\我的文檔\Visual Studio 2008\Projects\Accelerated\Accelerated\Debug\BuildLog.htm”
1>Accelerated - 6 個錯誤,0 個警告
========== 生成: 成功 0 個,失敗 1 個,最新 0 個,跳過 0 個 ==========

 

上面問題不知道怎么解決,就開始google解決方案: 模板不支持分離編譯, 把你模板類的聲明和實現放到.h文件里面 。按照這個說的把.h和.cpp文件合並后,果然可以了。

但是為什么呢,為什么模板就不支持分離編譯?---繼續google ing

搜到了如下文章(文章原文鏈接:http://blog.csdn.net/bichenggui/article/details/4207084):

首先,一個編譯單元(translation unit)是指一個.cpp文件以及它所#include的所有.h文件,.h文件里的代碼將會被擴展到包含它的.cpp文件里,然后編譯器編譯該.cpp文件為一個.obj文件(假定我們的平台是win32),后者擁有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 0xABCDEF。這個地址可能是任意的,然而關鍵是這個地址上有一行指令來進行真正的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<class T>

void f(T t)

{}

int main()

{

…//do something

f(10); // call f<int> 編譯器在這里決定給f一個f<int>的實例

…//do other thing

}

也就是說,如果你在main.cpp文件中沒有調用過f,f也就得不到實例化,從而main.obj中也就沒有關於f的任意一行二進制代碼!如果你這樣調用了:

f(10); // f<int>得以實例化出來

f(10.0); // f<double>得以實例化出來

這樣main.obj中也就有了f<int>,f<double>兩個函數的二進制代碼段。以此類推。

然而實例化要求編譯器知道模板的定義,不是嗎?

看下面的例子(將模板的聲明和實現分離):

//-------------test.h----------------//

template<class T>

class A

{

public:

void f(); // 這里只是個聲明

};

//---------------test.cpp-------------//

#include”test.h”

template<class T>

void A<T>::f() // 模板的實現

{

…//do something

}

//---------------main.cpp---------------//

#include”test.h”

int main()

{

A<int> a;

f(); // #1

}

編譯器在#1處並不知道A<int>::f的定義,因為它不在test.h里面,於是編譯器只好寄希望於連接器,希望它能夠在其他.obj里面找到A<int>::f的實例,在本例中就是test.obj,然而,后者中真有A<int>::f的二進制代碼嗎?NO!!!因為C++標准明確表示,當一個模板不被用到的時侯它就不該被實例化出來,test.cpp中用到了A<int>::f了嗎?沒有!!所以實際上test.cpp編譯出來的test.obj文件中關於A::f一行二進制代碼也沒有,於是連接器就傻眼了,只好給出一個連接錯誤。但是,如果在test.cpp中寫一個函數,其中調用A<int>::f,則編譯器會將其實例化出來,因為在這個點上(test.cpp中),編譯器知道模板的定義,所以能夠實例化,於是,test.obj的符號導出表中就有了A<int>::f這個符號的地址,於是連接器就能夠完成任務。

關鍵是:在分離式編譯的環境下,編譯器編譯某一個.cpp文件時並不知道另一個.cpp文件的存在,也不會去查找(當遇到未決符號時它會寄希望於連接器)。這種模式在沒有模板的情況下運行良好,但遇到模板時就傻眼了,因為模板僅在需要的時候才會實例化出來,所以,當編譯器只看到模板的聲明時,它不能實例化該模板,只能創建一個具有外部連接的符號並期待連接器能夠將符號的地址決議出來。然而當實現該模板的.cpp文件中沒有用到模板的實例時,編譯器懶得去實例化,所以,整個工程的.obj中就找不到一行模板實例的二進制代碼,於是連接器也黔驢技窮了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM