重載函式的問題
某一天我正在看《設計模式之禪》,看到P18,書上範例是 JAVA,我改成 C++之後寫出類似這樣的東西。繼承類別中的多載。
#include <iostream>
using namespace std;
class CFather
{
public:
void Test(int p){cout<<"CFather"<<endl;}
};
class CChild : public CFather
{
public:
void Test(){cout<<"CChild"<<endl;}
};
int main()
{
CChild *pC = new CChild;
CFather *pF = new CFather;
pC->Test(10); //compiler 不給過
delete pC;
delete pF;
system("pause");
return 0;
}
錯誤訊息是 error C2660: 'CChild::Test' : function does not take 1 arguments
書上還說「不在一個類別就不能是多載?繼承是什麼意思,子類別擁有父類別所有的屬性和方法。方法名稱相同,輸入參數型別又不相同,當然是多載了。」理論上好像這樣可以,那為什麼 C++ 不能這樣寫?問了幾個朋友和 Google 也得不到答案,只好翻書去了。
以下來自《C++ Primer 4/e》
重載函式(P265)
如果兩個函式擁有相同名稱、出現於同一作用域、參數列不同,這兩個函式就形成所謂的「重載」(overload)。
函式內宣告的 local 名稱會把 global 作用域內宣告的相同名稱遮蓋掉。不論函式名稱或變數名稱,情形都一樣。
int GetInt()
{
return 3;
}
int main()
{
int GetInt = 5;
cout<<GetInt()<<endl; //錯誤,被int GetInt遮蔽
system("pause");
return 0;
}
標準的作用域規則(scoping rules)亦適用於重載函式命名。如果我們區域性地(locally)宣告一個函式,該函式會遮掩掉外層作用域所宣告的同名函式,不會形成重載。因此重載函式的每一個版本的宣告都必須出現在同一作用域內。
void print(const string &s){};
void print(double d){};
int main()
{
int p = 3;
void print(int n); //新作用域內,遮蓋了先前的print
print("pp"); //錯誤print(const string)被遮蔽
print(p); //呼叫print(int n)
print(3.15); //呼叫print(int n)
system("pause");
return 0;
}
main 函式裡的 print(int) 宣告遮蓋了其他 print 宣告,於是只有一個 print 函式可用。
當呼叫 print 時,編譯器首先搜尋該名稱,他找到接收一個 int 的 print local 函式,一旦找到,編譯器就不再檢查外層作用域是否有同名函式存在。
C++的名稱搜尋(name lookup)發生於型別檢驗(type checking)之前。
重載決議(Overload Resolution)三步驟(P270)
1.候選函式(Candidate Function)
確認呼叫動作所考慮的重載函式集,此集合內的所有函式稱為候選函式。候選函式與被呼叫的函式名稱相同,其宣告式必須在呼叫點可見。
2.定出可行函式(Viable Functions)
從候選函式挑出可被「呼叫動作所指明的引數」喚起的函式,被選出的函式稱為可行函式。挑選條件有二:第一,參數個數要與被呼叫函式相同。第二,每個引數型別必須吻合,或是能夠轉換至對應參數的型別。
3.找出最契合函式(Best Match)
判斷哪個可行函式與呼叫動作所指明的實際引數最契合。這將檢查呼叫動作的每個引數,挑出與對應參數最契合的一或多個可行函式。所謂最契合,概念上是「引數和參數的型別關係越接近越契合」。
繼承情況下的 class 作用域(P592)
如果 base 和 derived class 擁有相同名稱的成員函式,其運作方式如同成員變數:derived 作用域內的成員會遮蔽 base class 的成員。即使函式原型不同,base 成員仍會受到遮蔽。
class Base
{
public:
void memfcn(){};
};
class Derived : public Base
{
public:
void memfcn(int){};
};
int main()
{
Base b;
Derived d;
b.memfcn(); //呼叫Base::memfcn()
d.memfcn(10); //呼叫Derived::memfcn(int)
d.memfcn(); //錯誤:無引數的memfcn已被遮蔽
d.Base::memfcn(); //呼叫Base::memfcn()
system("pause");
return 0;
}
d.memfcn(); 為了決議此一呼叫,編譯器搜尋 memfcn 名稱,並在 Derived class 發現。一旦找到名稱,編譯器就不再繼續尋找。而這個呼叫與 Derived 內的 memfcn() 定義不契合,因此導致錯誤。
回憶一下,宣告於 local 作用域的函式,並不會與定義於 global 作用域內的函式形成重載。同樣道理,定義於 derived class 的函式不會與 base 定義的函式形成重載。只有當 derived class 完全未定義該函式時,編譯器才會考慮 base class 的同名函式。
題外話
C# 和 java 可以過。
//測試C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public class Base
{
public void memfcn(int n)
{
System.Console.WriteLine("Base::memfcn");
}
};
public class Derived : Base
{
public void memfcn()
{
System.Console.WriteLine("Derived::memfcn");
}
};
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Base b = new Base();
Derived d = new Derived();
b.memfcn(20); //Base::memfcn
b.memfcn(); //錯誤
d.memfcn(10); //Base::memfcn
d.memfcn(); //Derived::memfcn
System.Console.Read();
}
}
}
//測試java
public class A
{
public void print()
{
System.out.println("A::Print");
}
}
public class B extends A
{
public void print(int n)
{
System.out.println("B::Print");
}
}
public class Test {
public static void main( String[] args ) {
A a = new A();
B b = new B();
a.print(); //A::Print
b.print(); //A::Print
b.print(10); //B::Print
}
}
如果想要讓第一個程式可以過的話,可以這樣寫。但是 C++ 似乎完全不建議你做這種事。
#include <iostream>
using namespace std;
class CFather
{
public:
void Test(int p){cout<<"CFather"<<endl;}
};
class CChild : public CFather
{
public:
using CFather::Test; //主要是這裡
void Test(){cout<<"CChild"<<endl;}
};
int main()
{
CChild *pC = new CChild;
CFather *pF = new CFather;
pC->Test(10);
delete pC;
delete pF;
system("pause");
return 0;
}