如何使用前置聲明取代包括頭文件

 

http://blog.csdn.net/rogeryi/archive/2006/12/12/1439597.aspx

 

這篇文章很大程度是受到Exceptional C++ (Hurb99)書中第四章 Compiler  Firewalls and the Pimpl Idiom  (編譯器防火牆和Pimpl慣用法) 的啓發,這一章講述了減少編譯時依賴的意義和一些慣用法,其實最爲常用又無任何副作用的是使用前置聲明來取代包括頭文件。

Item 26 的Guideline - "Never #include a header when a forward declaration will suffice"

在這裏,我自己總結了可以使用前置聲明來取代包括頭文件的各種情況和給出一些示例代碼。

首先,我們爲什麼要包括頭文件?問題的回答很簡單,通常是我們需要獲得某個類型的定義(definition)。那麼接下來的問題就是,在什麼情況下我們才需要類型的定義,在什麼情況下我們只需要聲明就足夠了?問題的回答是當我們需要知道這個類型的大小或者需要知道它的函數簽名的時候,我們就需要獲得它的定義。

假設我們有類型A和類型C,在哪些情況下在A需要C的定義:


A繼承至C
A有一個類型爲C的成員變量
A有一個類型爲C的指針的成員變量
A有一個類型爲C的引用的成員變量
A有一個類型爲std::list<C>的成員變量
A有一個函數,它的簽名中參數和返回值都是類型C
A有一個函數,它的簽名中參數和返回值都是類型C,它調用了C的某個函數,代碼在頭文件中
A有一個函數,它的簽名中參數和返回值都是類型C(包括類型C本身,C的引用類型和C的指針類型),並且它會調用另外一個使用C的函數,代碼直接寫在A的頭文件中
C和A在同一個名字空間裏面
C和A在不同的名字空間裏面

1,沒有任何辦法,必須要獲得C的定義,因爲我們必須要知道C的成員變量,成員函數。

2,需要C的定義,因爲我們要知道C的大小來確定A的大小,但是可以使用Pimpl慣用法來改善這一點,詳情請
看Hurb的Exceptional C++。

3,4,不需要,前置聲明就可以了,其實3和4是一樣的,引用在物理上也是一個指針,它的大小根據平臺不同,可能是32位也可能是64位,反正我們不需要知道C的定義就可以確定這個成員變量的大小。

5,不需要,有可能老式的編譯器需要。標準庫裏面的容器像list, vector,map,
在包括一個list<C>,vector<C>,map<C, C>類型的成員變量的時候,都不需要C的定義。因爲它們內部其實也是使用C的指針作爲成員變量,它們的大小一開始就是固定的了,不會根據模版參數的不同而改變。

6,不需要,只要我們沒有使用到C。

7,需要,我們需要知道調用函數的簽名。

8,8的情況比較複雜,直接看代碼會比較清楚一些。

            C& doToC(C&);
            C& doToC2(C& c) ...{return doToC(c);};
從上面的代碼來看,A的一個成員函數doToC2調用了另外一個成員函數doToC,但是無論是doToC2,還是doToC,它們的的參數和返回類型其實都是C的引用(換成指針,情況也一樣),引用的賦值跟指針的賦值都是一樣,無非就是整形的賦值,所以這裏即不需要知道C的大小也沒有調用C的任何函數,實際上這裏並不需要C的定義。

但是,我們隨便把其中一個C&換成C,比如像下面的幾種示例:

            1.
                C& doToC(C&);
            C& doToC2(C c) ...{return doToC(c);};
               
                2.
                C& doToC(C);
                C& doToC2(C& c) {return doToC(c);};

                3.
                C doToC(C&);
                C& doToC2(C& c) {return doToC(c);};

                4.
                C& doToC(C&);
                C doToC2(C& c) {return doToC(c);};
無論哪一種,其實都隱式包含了一個拷貝構造函數的調用,比如1中參數c由拷貝構造函數生成,3中doToC的返回值是一個由拷貝構造函數生成的匿名對象。因爲我們調用了C的拷貝構造函數,所以以上無論那種情形都需要知道C的定義。

9和10都一樣,我們都不需要知道C的定義,只是10的情況下,前置聲明的語法會稍微複雜一些。

最後給出一個完整的例子,我們可以看到在兩個不同名字空間的類型A和C,A是如何使用前置聲明來取代直接包括C的頭文件的:

A.h

#pragma once

#include <list>
#include <vector>
#include <map>
#include <utility>

    //不同名字空間的前置聲明方式
namespace test1
...{
          class C;
}

 

namespace test2
...{  
       //用using避免使用完全限定名
    using test1::C;
   
    class A
    ...{
    public:
                C   useC(C);
            C& doToC(C&);
            C& doToC2(C& c) ...{return doToC(c);};
                        
    private:
            std::list<C>    _list;
            std::vector<C>  _vector;
            std::map<C, C>  _map;
            C*              _pc;
            C&              _rc;
   
    };
}

C.h

#ifndef C_H
#define C_H
#include <iostream>

namespace test1
...{
         
    class C
    ...{
    public:
           void print() ...{std::cout<<"Class C"<<std::endl;}
    };

}

#endif // C_H

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/rogeryi/archive/2006/12/12/1439597.aspx

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