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

轉自csqlwy的博客園(http://www.cnblogs.com/qlwy/archive/2012/03/21/2410045.html)非常感謝這位老師的總結
/////  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中就找不到一行模板實例的二進制代碼,於是連接器也黔驢技窮了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章