C++中幾個值得分析的小問題(2)

QingLiXueShi發表於2015-04-29

下面有3個小問題,作為C++ Beginner你一定要知道錯在哪裡了。

1、派生類到基類的引用或指標轉換一定“完美”存在?

一般情況,你很可能會認為:派生類物件的引用或指標轉換為基類物件的引用或指標是一件很正常的事。那要是不一般情況呢?請看下面這個例子:

class Person
{
public:
    Person(const string& str = "Normal Person") : ID(str) {}
    string ID;                                //作為一般的人身份是“普通人”,作為學生身份是“學生”
};

class Student : private Person
{
public:
    Student(const string& str = "Student") : Person(str) {}
};

仍然是以普通人和學生作為例子,學生是private派生自普通人的。ID表示身份狀態(我將它宣告為public的,完全是為了方便,我仍然強烈建議宣告為private)。

void PrintID(const Person& p)            //列印ID
{
    cout << p.ID << endl;
}

我有下面這樣的呼叫:

Person p;
Student s;
PrintID(p);            //好的,沒有問題
PrintID(s);            //編譯出錯

好的,我已經註釋出來了。編譯出錯,出錯資訊是:error C2243: “型別轉換”: 從“Student *”到“const Person &”的轉換存在,但無法訪問。是不是有點跟你想的不大一樣呢?

分析:

很多時候,我們都預設public繼承,public繼承勾勒出一種is-a的關,就是派生類是一種基類,從這個語義上理解似乎從派生類轉換為基類的自動的(引用或指標方式)。我這裡說的是private繼承,private繼承勾勒出一種“根據某物實現出”的關係。假設D以private派生於B,意思是D可以根據B物件實現得到。編譯器不會自動將派生類物件轉換為基類物件。

另外一點疑問,你可能注意到了PrintID如果傳入的是Student物件,似乎訪問了私有成員ID(private繼承),是不是這個原因導致編譯錯誤呢?我們將其改為下面這樣:

void PrintID(const Person& p)            //列印ID
{
    cout << "ID Infor" << endl;
//    cout << p.ID << endl;
}

同樣編譯錯誤。所以,要注意:派生類到基類的引用或指標轉換一定“完美”存在,前提是public繼承,不能是private或protected繼承

說明:還要補充說明一下,在類的繼承層次中public為主,private有時候在設計上可以提供一點“便利”,protected我暫時還不知道有什麼好的用處。

 


2、關鍵字typename導致的編譯時錯誤。

這個問題,可能很多人都知道,也許像我一樣知道的“不全”。首先需要明確一點是:關鍵字typename與class在宣告template型別引數時完全一樣。我其實要說的是typename泛型(模板)中的使用,它可是大有所為的。

template內出現的名稱如果依賴於某個template引數,稱之為從屬名稱。如果從屬名稱在class內呈巢狀狀,稱之為巢狀從屬名稱。

template <typename T>
void PrintElement(const T& vec)
{
    T::const_iterator* x;            //const_iterator是個巢狀從屬名稱
...
}

我們可能認為宣告瞭一個指標,指向一個T::const_iterator。實際上,T::const_iterator也可能是個名字為const_iterator的static變數,上面那行程式碼就定義了一個乘法。所以,巢狀從屬名稱可能導致解析困難

C++解析器有個簡單的規則:在template中遇到一個巢狀從屬名稱,它會假設這個名稱不是個型別,這是預設情況,除非你用關鍵字typename主動告訴編譯器。

template <typename T>
void PrintElement(const T& vec)
{
    typename T::const_iterator iter;            //這樣宣告就對了
...
}

這塊知識,基本每個人都知道。我其實想說的是,使用typename時,有幾個小點注意一下,因為typename的位置不僅在函式內。比如:

template <typename T>
void Print(typename T::const_iterator iter)        //形參表一定要使用typename

下面再舉個複雜一點的例子:

template <class T>
class Base 
{
public:
    class Nested 
    {
    public:
        Nested(int a) : x(a) {}
    private:
        int x;
    };
};

template<class T>
class Derived : public Base<T>::Nested                    //繼承列表,你不能使用typename
{
public:
    explicit Derived(int a) : Base<T>::Nested(a){}        //建構函式初始化列表,你不能使用typename
};

注意,程式碼中註釋的部分,就是typename兩個不能使用的地方。我再擴充套件一下這個問題:

template<class T>
void func(const typename Base<T>::Nested& test)            //使用typename,很好的
{
    cout << "Very Good" << endl;            
}

我想問的是:Derived<int> d(5);怎麼呼叫那個函式呢?正確是策略是:

func<int> (d);                //正確的
func(d);                    //錯誤的

錯誤的用法,編譯器會提醒你:無法解析T。

 


3、建構函式、解構函式能否為虛擬函式?建構函式、解構函式能否呼叫虛擬函式?

這個小問題,真是源遠流長了。標準答案是不是都有了呢?我再不耐其煩的說一下。(我沒有把各家說法收集全,就是說下自己的理解)

(1)建構函式、解構函式能否為虛擬函式?

建構函式不能為虛擬函式。

回答:有好幾個點可以說說,我說的不全。

1、虛擬函式的作用主要是為了執行期根據Base指標定址到“正確”的函式。既然是執行期,必然已經跨過編譯期了,那勢必物件都建立起來了,建構函式是初始化用的,那要建構函式何用。

2、如果我沒記錯的話,建構函式有一個功能是建立vptr,注意不是vtbl(編譯期搞定的),假設是虛擬函式,誰來建立vptr,此時建構函式應該在vtbl裡待著呢。

3、虛擬函式是為了派生類對其進行改造的,但建構函式不能被繼承,談何改造。

等等…

解構函式能不能為虛擬函式?

如果像STL那樣堅決不做派生類,我勸你別把它當虛擬函式,vptr好歹也是4個位元組的儲存呢。如果你要它作為派生類,我勸你一定要設定為虛擬函式,否則記憶體洩露會找上你。

(2)建構函式、解構函式能否呼叫虛擬函式?

其實這個問題,我一直都沒明白有啥意義。建構函式、解構函式呼叫虛擬函式是可以的,可是呼叫的是所屬類的版本

 


目錄:

C++中幾個值得分析的小問題(1)

C++中幾個值得分析的小問題(2)

相關文章