1. 程式人生 > >C++中派生類重寫基類過載函式時需要注意的問題:派生類函式遮蔽基類中同名函式

C++中派生類重寫基類過載函式時需要注意的問題:派生類函式遮蔽基類中同名函式

派生類可以繼承基類中的非私有函式成員,當然也就可以繼承其中非私有的被過載的函式。如下:

【參考程式碼】

class Base {
 public:
  void print() {
    cout << "print() in Base." << endl;
  }
  void print(int a) {
    cout << "print(int a) in Base." << endl;
  }
  void print(string s) {
    cout << "print(string s) in Base." << endl;
  }
};


class Derived : public Base { };


int main() {
  Derived d;
  d.print();
  d.print(10);
  d.print("");
  return 0;
}
【執行結果】
print() in Base.
print(int a) in Base.
print(string s) in Base.

現在,我們想要在派生類中重寫其中的一個過載函式:
class Derived : public Base {
 public:
  void print() {
    cout << "Rewrite print() in Derived." << endl;
  }
};
這樣是不是就可以了呢? 我們來執行一下:

【執行結果】

reload_test.cc: In function ‘int main()’:
reload_test.cc:39: error: no matching function for call to ‘Derived::print(int)’
reload_test.cc:21: note: candidates are: void Derived::print()
reload_test.cc:40: error: no matching function for call to ‘Derived::print(const char [1])’
reload_test.cc:21: note: candidates are: void Derived::print()
結果出錯了,顯示說匹配不到後兩種情況,這是為什麼呢?

下面一段內容來自 C++ Primer:

理解 C++ 中繼承層次的關鍵在於理解如何確定函式呼叫。確定函式呼叫遵循以下四個步驟:

1. 首先確定進行函式呼叫的物件、引用或指標的靜態型別。

2. 在該類中查詢函式,如果找不到,就在直接基類中查詢,如此循著類的繼承鏈往上找,直到找到該函式或者查詢完最後一個類。如果不能在類或其相關基類中找到該名字,則呼叫是錯誤的。

3. 一旦找到了該名字,就進行常規型別檢查,檢視如果給定找到的定義,該函式呼叫是否合法。

4. 假定函式呼叫合法,編譯器就生成程式碼。如果函式是虛擬函式且通過引用或指標呼叫,則編譯器生成程式碼以確定根據物件的動態型別執行哪個函式版本,否則,編譯器生成程式碼直接呼叫函式。

         原來,C++中,每個類都記錄著在該類中定義的函式名及型別資訊,當發生函式呼叫時,編譯器先按函式名查詢,如果在該類中查不到與之匹配的函式名,則向其父類查詢,依次向上遞迴,直至函式名匹配成功,然後進行引數型別等資訊的匹配;或者查到最頂層仍未匹配到相應的函式名。

         所以,當我們在派生類中沒有重寫過載函式之一的時候,在派生類中呼叫的過載函式是在其基類中查到的,因此,呼叫可以成功;然而,當我們僅重寫了其中的一個過載函式時,在做函式名匹配時,在本類中就可以匹配到了,就不會向其父類查找了。而在派生類中,僅記錄了這個被重寫的函式的資訊,當然也就沒有另外兩個過載函式的一些了,因此就導致了上述錯誤的出現了。  換句話說,派生類中的函式會將其父類中的同名函式遮蔽掉。

        因此,如果派生類想通過自身型別使用的基類中過載版本,則派生類必須要麼重定義所有過載版本,要麼一個也不重定義。

        那麼,如果在派生類中需要且僅需要重寫其中一個過載函式,必須得把其它過載函式都重定義嗎?有沒有簡便的方法,僅重定義我們想要改變的那個,其它的還是從父類繼承呢。答案是肯定的,有,而且還可以有不同的方式:

一、通過using在派生類中為父類函式成員提供宣告:

         前面知道,因為派生類重寫的函式名遮蔽了父類中的同名函式,那麼我們可以通過using來為父類函式提供宣告;這樣,派生類不用重定義所繼承的每一個基類版本,它可以為過載成員提供 using宣告。一個 using 宣告只能指定一個名字,不能指定形參表,因此,為基類成員函式名稱而作的 using 宣告將該函式的所有過載例項加到派生類的作用域。將所有名字加入作用域之後,派生類只需要重定義本型別確實必須定義的那些函式,對其他版本可以使用繼承的定義。

上面說了那麼多,不知道說明白了沒有,不過,看了下面的例子,你就會豁然開朗: so easy!

class Derived : public Base {
 public:
  using Base::print;
  void print() {
    cout << "print() in Derived." << endl;
  }
};
僅僅需要加入  using Base::print;  問題便解決了:

【執行結果】

print() in Derived.
print(int a) in Base.
print(string s) in Base.

二、通過基類指標呼叫

        在呼叫被遮蔽的過載函式時,可以不直接通過派生類物件呼叫,而是通過基類指標指向派生類物件,通過基類指標進行呼叫,這樣就會直接在基類中進行查詢函式名,從而可以匹配並進行型別匹配。

int main() {
  Derived d;
  Base* bp = &d; 
  d.print();
  bp->print(10);
  bp->print("");
  return 0;
}
【執行結果】
print() in Derived.
print(int a) in Base.
print(string s) in Base.

        但是這樣就有兩種呼叫方式,看起來很不舒服,而且容易弄錯。那麼把在派生類中需要過載的那個版本相應地在基類中宣告為vitual,從而可以實現動態繫結,就能統一的使用基類指標來呼叫了:

【參考程式碼】

class Base {
 public:
  virtual void print() {
    cout << "print() in Base." << endl;
  }
  void print(int a) {
    cout << "print(int a) in Base." << endl;
  }
  void print(string s) {
    cout << "print(string s) in Base." << endl;
  }
};

class Derived : public Base {
 public:
  void print() {
    cout << "print() in Derived." << endl;
  }
};

int main() {
  Derived d;
  Base* bp = &d; 
  bp->print();
  bp->print(10);
  bp->print("");
  return 0;
}
【執行結果】
print() in Derived.
print(int a) in Base.
print(string s) in Base.