C++中重載、覆蓋以及隱藏的區別
C++中重載、覆蓋以及隱藏是經常讓人混淆的三個概念。
1、重載
有兩個或多個函數名相同的函數,但是函數的形參列表不同。在調用相同函數名的函數時,根據形參列表確定到底該調用哪一個函數。
重載是C++提供的一種靈活運用的操作,它不止可以用到函數,還可以運用到運算符中進行重載。
重載的函數必須位於同一個命名空間中,在類的層次上來看,每個類就相當於是一個命名空間,所以重載只能發生在同一個類中。
成員函數被重載的特徵:
(1)相同的範圍(在同一個類中);
(2)函數名字相同;
(3)參數不同;
(4)virtual 關鍵字可有可無。
舉例:
class Base
{
public:
void f(int x){ cout << "Base::f(int) " << x << endl; }
void f(float x){ cout << "Base::f(float) " << x << endl; }
virtual void g(void){ cout << "Base::g(void)" << endl;}
};
int main()
{
Derived d;
d.f(42);
d.f(3.14f);
return 0;
}
運行結果:
2、覆蓋
在基類中定義了一個虛擬函數,然後在派生類中又定義了一個同名同參數同返回類型的函數,這就是覆蓋了。
在派生類對象上直接調用這個函數名,只會調用派生類中的那個。
覆蓋是發生在不同類之間(基類和派生類)。
覆蓋是指派生類函數覆蓋基類函數,特徵是:
(1)不同的範圍(分別位於派生類與基類);
(2)函數名字相同;
(3)參數相同;
(4)基類函數必須有virtual 關鍵字。
舉例:
class Base
{
public:
void f(int x){ cout << "Base::f(int) " << x << endl; }
void f(float x){ cout << "Base::f(float) " << x << endl; }
virtual void g(void){ cout << "Base::g(void)" << endl;}
};
class Derived : public Base
{
public:
void g(void){ cout << "Derived::g(void)" << endl;}
};
int main()
{
Derived d;
Base *pb = &d;
pb->f(42); // Base::f(int) 42
pb->f(3.14f); // Base::f(float) 3.14
pb->g(); // Derived::g(void)
return 0;
}
運行結果:
注意:此時如果將基類中函數g()前的virtual去掉,會是什麼結果?答案是,這種情況不會構成覆蓋。
2、覆蓋
既然是和虛擬函數掛鉤,說明了這個是一個多態支持的特性,所謂的覆蓋指的是用基類對象的指針 或者引用時訪問虛擬函數的時候會根據實際
的類型決定所調用的函數,因此此時派生類的成員函數可以"覆蓋"掉基類的成員函數. 注意唯有同名且參數相同還有帶有virtual關鍵字並且分別
在派生類和基類的函數才能構成虛擬函數,這個也是派生類的重要特徵. 而且,由於是和多態掛鉤的,所以只有在使用類對象指針或者引用的時候
才能使用上. 總之一句話:覆蓋函數都是虛函數,反之不然~~
(如果基類和繼承類的函數名稱,產生返回值都是一樣的[如果返回值不同應該無法
編譯],如果基類用到了virtual,那麼無論繼承類的實現中是否加入virtual 這個keyword ,還是會構成覆蓋的關係)。
多態:
在基類中定義了一個虛擬函數,然後在派生類中又定義一個同名,同參數表的函數,這就是多態。多態是這3種
情況中唯一採用動態綁定技術的一種情況。也就是說,通過一個基類指針來操作對象,如果對象是基類對象,
就會調用基類中的那個函數,如果對象實際是派生類對象,就會調用派聲類中的那個函數,調用哪個函數並不
由函數的參數表決定,而是由函數的實際類型決定。
舉例:
class A
{
public:
virtual void ShowMessage(){cout<<"Hello,This is A.\n";} // 分析加不加virtual的區別
};
class B:public A
{
public:
void ShowMessage(){cout<<"Hello,This is B.\n";}
};
int main()
{
A* p;
p=new A();
p->ShowMessage();
p=new B();
p->ShowMessage();
return 0;
}
運行結果:
3、隱藏
指的是派生類的成員函數隱藏了基類函數的成員函數.隱藏一詞可以這麼理解:在調用一個類的成員函數的時候,編譯器會沿着類的繼承鏈逐級
的向上查找函數的定 義,如果找到了那麼就停止查找了,所以如果一個派生類和一個基類都有同一個同名(暫且不論參數是否相同)的函數,而編
譯器最終選擇了在派生類中的函數,那麼我們就說這個派生類的成員函數"隱藏"了基類的成員函數,也就是說它阻止了編譯器繼續向上查找函數
的定義。回到隱藏的定義中,前面已經說了有virtual關鍵字並且分別位於派生類和基類的同名,同參數函數構成覆蓋的關係,因此隱藏的關係只有
如下的可能:
1)必須分別位於派生類和基類中 ;
2)必須同名;
3)參數不同的時候本身已經不構成覆蓋關係了,所以此時是否是virtual函數已經不重要了。
當參數相同的時候就要看時候有virtual關鍵字了,有的話就是覆蓋關係,沒有的時候就是隱藏關係了很多人分辨不清隱藏和覆蓋的區別,因爲他們
都是發生在基類和派生類之中的.但是它們之間最爲重要的區別就是: 覆蓋的函數是多態的,是存在於vtbl之中的函數才能構成"覆蓋"的關係,而
隱藏的函數都是一般的函數,不支持多態,在編譯階段就已經確定下來了。
直接使用類對象纔會出現隱藏。
舉例:
struct foo
{
void func(int x)
{
cout <<"foo::func(int)"<<endl;
}
void func(float x)
{
cout <<"foo::func(float)"<<endl;
}
void func(double x)
{
cout <<"foo::func(double)"<<endl;
}
};
struct a
{
void func(int x)
{
cout <<"struct a :: fun(int x)"<<endl;
}
};
struct b : public a
{
void func(float x)
{
cout <<"struct b::fun(float x)"<<endl;
}
};
struct c : public b
{
//using a::func;
//using b::func;
void func(double x)
{
cout <<"struct c::func(double x)"<<endl;
}
};
int main()
{
foo fo;
fo.func(1);
fo.func((float)1);
c oc; // 直接根據類對象來調用
oc.func(1);
oc.func((float)1);
}
注意:怎麼樣取消隱藏?可以申明出基類命名空間(每個類都相當於是一個獨立的命名空間),這樣隱藏就變成了重載,因爲不同的函數成
爲了一個命名空間(注意,此時基類和派生類中的同名函數的參數一定要不同)。
struct foo
{
void func(int x)
{
cout <<"foo::func(int)"<<endl;
}
void func(float x)
{
cout <<"foo::func(float)"<<endl;
}
void func(double x)
{
cout <<"foo::func(double)"<<endl;
}
};
struct a
{
void func(int x)
{
cout <<"struct a::fun(int x)"<<endl;
}
};
struct b : public a
{
void func(float x)
{
cout <<"struct b::fun(float x)"<<endl;
}
};
struct c : public b
{
using a::func;
using b::func;
void func(double x)
{
cout <<"struct c::func(double x)"<<endl;
}
};
int main()
{
foo fo;
fo.func(1);
fo.func((float)1);
c oc;
oc.func(1);
oc.func((float)1);
}
運行結果:
總結:在基類和派生類中,如果直接使用類對象進行操作,容易引起隱藏。當使用指針調用類的操作時,如果該成員函數已用virtual進行
了聲明,那麼會發生覆蓋,這時候具體的操作應該依據指針所指的對象類型;反之,當沒有virtual時,不會發生覆蓋,這時候具體的操作
要依據指針的類型來進行判斷。
舉例:
class Base
{
public:
virtual void f(float x){ cout << "Base::f(float) " << x << endl; }
void g(float x){ cout << "Base::g(float) " << x << endl; } // 加不加virtual沒有關係,派生類與基類該函數的參數不同
void h(float x){ cout << "Base::h(float) " << x << endl; }
};
class Derived : public Base
{
public:
virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }
void g(int x){ cout << "Derived::g(int) " << x << endl; }
void h(float x){ cout << "Derived::h(float) " << x << endl; }
};
int main()
{
Derived d;
Base *pb = &d;
Derived *pd = &d;
// Good : behavior depends solely on type of the object(定義爲虛函數virtual)
pb->f(3.14f); // Derived::f(float) 3.14
pd->f(3.14f); // Derived::f(float) 3.14
// Bad : behavior depends on type of the pointer(未定義爲虛函數virtual)
pb->g(3.14f); // Base::g(float) 3.14
pd->g(3.14f); // Derived::g(int) 3 (surprise!)
// Bad : behavior depends on type of the pointer
pb->h(3.14f); // Base::h(float) 3.14 (surprise!)(未定義爲虛函數virtual)
pd->h(3.14f); // Derived::h(float) 3.14
// 隱藏基類的操作
d.f(3.14f);
d.g(3.14f);
d.h(3.14f);
return 0;
}
運行結果:
最後,再提醒一個值得注意的地方,經常遇到的虛析構函數,實質上就是覆蓋操作。虛析構函數(基類的析構函數前+virtual)的使用主要
是爲了防止內存的泄露,詳情見我上篇博文《深度解析構造函數和析構函數》(http://blog.csdn.net/shenbo2030/article/details/44
588203)。
舉例:
class ClxBase
{
public:
ClxBase() {};
virtual ~ClxBase() {cout << "Output from the destructor of class ClxBase!" << endl;} // 加不加virtual的區別
virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; } // 加不加virtual的區別
};
class ClxDerived : public ClxBase
{
public:
ClxDerived() {};
~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; }
void DoSomething() { cout << "Do something in class ClxDerived!" << endl; }
};
int main()
{
//ClxBase Test;
ClxBase *pTest = new ClxDerived;
pTest->DoSomething();
delete pTest;
return 0;
}
運行結果:
如果基類中析構函數前不加virtual,則運行結果如下(派生類沒有進行釋放),此時會造成內存泄露,產生無法預測的錯誤。