C++繼承詳解之二——派生類成員函式詳解(函式隱藏、建構函式與相容覆蓋規則)
在這一篇文章開始之前,我先解決一個問題。
在上一篇C++繼承詳解之一——初探繼承中,我提到了在派生類中可以定義一個與基類成員函式同名的函式,這樣派生類中的函式就會覆蓋掉基類的成員函式。
在譚浩強的C++程式設計這本書第十一章,351頁最下面有這麼一段話:
可在派生類中宣告一個與基類成員同名的成員函式,則派生類中的新函式會覆蓋基類的同名成員,但應注意:如果是成員函式,不僅應是函式名相同,而且函式的引數表(引數的個數和型別)也應相同,如果不相同,就會成為函式過載而不是覆蓋了、用這樣的方法可以用新成員取代基類的成員。
但是經過我的實驗,這段話就是錯誤的,派生類中定義與基類成員函式同名不同引數表的函式是不能構成函式過載的,先上程式碼:
class A
{
public:
int a_data;
void a()
{
cout << "A" << endl;
}
};
class B
{
public:
int b_data;
void b()
{
cout << "B" << endl;
}
};
class C :public A, public B
{
public:
int c_data;
void a(int data)//過載A類中的a()函式
{
cout << "C" << endl;
}
};
int main()
{
C c;
c.a();
return 0;
}
編譯後,編譯器會報錯
Error 1 error C2660: 'C::a' : function does not take 0 arguments e:\demo\繼承\way\project1\project1\source.cpp 86 1 Project1
2 IntelliSense: too few arguments in function call e:\DEMO\繼 承\way\Project1\Project1\Source.cpp 86 6 Project1
錯誤表明:編譯器並沒有將c.a()看做C類繼承自A類的a()函式,而是報錯沒有給a函式引數,即不構成函式過載,如果給c.a(1)一個引數,編譯通過。輸出:C
那麼我們不給C類中定義同名函式呢
class A
{
public:
int a_data;
void a()
{
cout << "A" << endl;
}
};
class B
{
public:
int b_data;
void b()
{
cout << "B" << endl;
}
};
class C :public A, public B
{
public:
int c_data;
//void a(int data)
//{
// cout << "C" << endl;
//}
};
int main()
{
C c;
c.a();
return 0;
}
編譯通過,執行輸出:A
以上兩個例子,完全可以說明,當我們在派生類中定義一個同名函式的時候,編譯器是將同名函式隱藏了,不管引數表是否相同。即不會構成函式過載,直接為函式覆蓋。
那麼問題來了,為什麼不會構成函式過載呢?
一定要注意,函式過載的條件是在同一個作用域中才會構成函式過載,而派生類和基類是兩個類域,一定不會構成函式過載的
現在進入這篇文章的主題,派生類成員函式的解答,因為開篇我們講的這個例子,我先從函式覆蓋寫起。
一、函式覆蓋、函式隱藏、函式過載
先分別講一下函式覆蓋,隱藏和過載分別是什麼:
1.成員函式被過載的特徵
(1)相同的範圍(在同一個類中);
(2)函式名字相同;
(3)引數不同;
(4)virtual 關鍵字可有可無。
//virtual關鍵字在這裡看不懂也可以,在下一篇文章中我會詳細解答
覆蓋是指派生類函式覆蓋基類函式,特徵是
(1)不同的範圍(分別位於派生類與基類);
(2)函式名字相同;
(3)引數相同;
(4)基類函式必須有virtual 關鍵字。
當派生類物件呼叫子類中該同名函式時會自動呼叫子類中的覆蓋版本,而不是父類中的被覆蓋函式版本,這種機制就叫做覆蓋。
派生類物件呼叫的是派生類的覆蓋函式
指向派生類的基類指標呼叫的也是派生類的覆蓋函式
基類的物件呼叫基類的函式
“隱藏”是指派生類的函式遮蔽了與其同名的基類函式,規則如下
(1)如果派生類的函式與基類的函式同名,但是引數不同。此時,不論有無virtual關鍵字,基類的函式將被隱藏(注意別與過載混淆)。
//這裡就是我們開篇舉得那個例子
(2)如果派生類的函式與基類的函式同名,並且引數也相同,但是基類函式沒有virtual 關鍵字。此時,基類的函式被隱藏(注意別與覆蓋混淆)
有關函式覆蓋與隱藏的詳細解釋,我會放在下一篇或者下下一篇文章中詳細解釋,這篇是為了提一下,將基本知識點講完後再回來分析這個問題。
二、派生類的預設成員函式
下面用公有繼承舉例
派生類物件包含基類物件,使用公有繼承,基類的公有成員將成為派生類的公有成員,基類的私有部分也將成為派生類的一部分,但只能通過基類的公有和保護方法訪問。
class Base
{};
class Derive:public Base //公有繼承
{};
那麼上面的程式碼完成了哪些工作呢?
Derive類具有以下特徵:
1.派生類物件儲存了基類的資料成員(派生類繼承了基類的實現)
2.派生類物件可以使用基類的方法(派生類繼承了基類的介面)
那麼我們還需要給派生類新增什麼呢?
1.派生類需要自己的建構函式
2.派生類可以根據需要新增額外的資料成員和成員函式。
1.建構函式
派生類不能直接訪問基類的私有成員,而必須通過基類方法進行訪問。
即派生類建構函式必須使用基類建構函式。
建構函式不同於其他類的方法,因為它建立新的物件,而其他類的方法僅僅是被現有的物件呼叫,這是建構函式不能被繼承的一個原因。
繼承意味著派生類物件可以使用基類的方法,然而建構函式在完成其工作之前,物件並不存在。
在建立派生類物件時,程式首先建立基類物件,即基類物件應當在程式進入派生類建構函式之前被建立。
現在看下面的程式碼就可以理解這個順序:
class Base
{
public:
Base(int a = 0,int b = 0,int c = 0)
:_pub(a)
, _pro(b)
, _pri(c)
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
int _pub;
protected:
int _pro;
private:
int _pri;
};
class Derive :public Base
{
public:
Derive()
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
private:
int d_pri;
protected:
int d_pro;
public:
int d_a;
};
int main()
{
Derive a;
return 0;
}
執行結果為:
這就說明了,在建立派生類物件時,先呼叫基類的建構函式,再呼叫派生類的建構函式,而析構的順序相反。
這是因為在建立時是在棧內進行的,棧有著先進後出的屬性,所以先建立的後析構,後建立的先析構。
在上面的程式碼中是單繼承的情況,那麼多繼承的情況呢?
看下面的程式碼:
class Base
{
public:
Base(int a = 0,int b = 0,int c = 0)
:_pub(a)
, _pro(b)
, _pri(c)
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
int _pub;
protected:
int _pro;
private:
int _pri;
};
class Base1
{
public:
Base1()
{
cout << "base1" << this << endl;
}
~Base1()
{
cout << "~Base1" << endl;
}
};
class Derive :public Base,public Base1
{
public:
Derive()
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
private:
int d_pri;
protected:
int d_pro;
public:
int d_a;
};
int main()
{
Derive a;
return 0;
}
執行結果為:
從執行結果可以看到,在多繼承時,呼叫建構函式的順序與繼承列表的順序也是有關的,如果我們將程式碼中派生類的繼承列表改為:
class Derive:public Base1,public Base
{/*不變*/};
結果為:
這就是在上一篇我曾講過的,多繼承中繼承列表與派生類物件模型的關係。多繼承時派生類的物件模型是與繼承列表的順序相關的。
也可以理解為,因為派生類的物件模型中,基類成員在模型的最上面,所以要先呼叫基類的建構函式,再呼叫派生類的建構函式。
在上述程式碼中,基類是由預設的建構函式的,我們在Base的建構函式中給了它預設值,那麼,如果基類沒有預設的建構函式,可以嗎?
我們將程式碼改為:
class Base
{
public:
Base(int a,int b ,int c )//不給預設引數
:_pub(a)
, _pro(b)
, _pri(c)
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
int _pub;
protected:
int _pro;
private:
int _pri;
};
class Derive :public Base
{
public:
Derive()
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
private:
int d_pri;
protected:
int d_pro;
public:
int d_a;
};
int main()
{
Derive a;
return 0;
}
編譯不通過,給出的錯誤為:
Error 1 error C2512: 'Base' : no appropriate default constructor available e:\demo\繼承\way\project1\project1\source.cpp 28 1 Project1
2 IntelliSense: no default constructor exists for class "Base" e:\DEMO\繼承\way\Project1\Project1\Source.cpp 29 2 Project1
基類中沒有可用的建構函式。
那麼在建立派生類物件時,如果沒有預設的建構函式,我們如何在建立派生類物件之前,先建立基類物件呢?
還記得C++中的成員初始化列表嗎?
在C++中,成員初始化列表句法可以完成這個工作。
我們在定義派生類時,將程式碼改為:
class Base
{
public:
Base(int a ,int b ,int c )
:_pub(a)
, _pro(b)
, _pri(c)
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
void Show()
{
cout << "_pri" << _pri << endl;
//_pri成員不能在派生類中被訪問
//Show函式的目的是在派生類中也能輸出基類私有成員的狀態
cout << "_pro" << _pro << endl;
cout << "_pub" << _pub << endl;
}
int _pub;
protected:
int _pro;
private:
int _pri;
};
class Derive :public Base
{
public:
Derive()
:Base(1,2,3)
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
void Display()
{
Show();
//派生類不能訪問基類中的私有成員
//要是想打印出基類私有成員的狀態,只能在基類中定義成員函式
//再在派生類成員函式中呼叫它
}
private:
int d_pri;
protected:
int d_pro;
public:
int d_a;
};
int main()
{
Derive a;
a.Dispay();
return 0;
}
執行成功,結果為:
如果我們要在類外給基類成員賦值,那麼將派生類定義改為:
class Derive :public Base
{
public:
Derive(int a,int b,int c)
:Base(a,b,c)
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
void Display()
{
Show();
}
private:
int d_pri;
protected:
int d_pro;
public:
int d_a;
};
int main()
{
Derive a(1,2,3);
a.Display();
return 0;
}
執行成功,結果為:
這個時候的關係為:
總結以下上面的內容:
1.基類物件首先被建立
2.派生類建構函式應通過成員初始化列表將基類資訊傳遞給基類建構函式
3.派生類建構函式應初始化派生類新增的資料成員(這個在上面程式碼中沒有體現)**
建立派生類物件時,程式首先呼叫基類的建構函式,然後在呼叫派生類的建構函式,(與派生類物件模型有關),基類建構函式負責初始化派生類繼承的資料成員,派生類的建構函式主要用於初始化新增的資料成員。
派生類的建構函式總是滴啊用一個基類建構函式。
可以使用初始化列表句法指明要使用的基類建構函式,否則將使用預設的基類建構函式。
派生類物件析構時,程式首先呼叫派生類解構函式,再呼叫基類解構函式。
2.拷貝建構函式(也稱複製建構函式)
拷貝建構函式接受其所屬類的物件為引數。
在下述情況下,將使用拷貝建構函式
- 將新的物件初始化為一個同類物件
- 按值將物件傳遞給函式
- 函式按值返回物件
編譯器生成臨時物件
如果程式沒有顯式定義拷貝建構函式,編譯器將自動生成一個。
當然,如果想在派生類中構造基類物件,那麼不僅僅可以用建構函式,也可以用拷貝建構函式
class Derive :public Base
{
public:
Derive(const Base &tp)
:Base(tp)//拷貝建構函式
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
void Display()
{
Show();
}
private:
int d_pri;
protected:
int d_pro;
public:
int d_a;
};
int main()
{
Base b(1, 2, 3);
Derive a(b);
a.Display();
return 0;
}
執行成功,結果為:
這裡我沒有給基類定義拷貝建構函式,但是編譯器自動給基類生成了一個拷貝建構函式,因為我基類中定義的沒有指標成員,所以淺拷貝可以滿足我的要求,但是如果在基類成員中有指標變數,必須要進行顯式定義拷貝建構函式,即進行深拷貝。不然會造成同一塊記憶體空間被析構兩次的問題。
3.賦值操作符
預設的賦值操作符用於處理同類物件之間的賦值,賦值不是初始化,如果語句建立新的物件,則使用初始化,如果語句修改已有物件的值,則為賦值。
注意:賦值運算和拷貝構造是不同的,賦值是賦值給一個已有物件,拷貝構造是構造一個全新的物件
class Base
{};
int main()
{
Base a;
Base b = a;//初始化
Base c;
c = a;//賦值
}
賦值運算子是不能被繼承的,原因很簡單。派生類繼承的方法的特徵與基類完全相同,但賦值操作符的特徵隨類而異,因為它包含一個型別為其所屬類的形參。
如果編譯器發現程式將一個物件賦給同一個類的另一個物件,它將自動為這個類提供一個賦值操作符。這個操作符的預設版本將採用成員賦值,即將原物件的相應成員賦給目標物件的每個成員。
如果物件屬於派生類,編譯器將使用基類賦值操作符來處理派生物件中基類部分的賦值,如果顯示的為基類提供了賦值操作符,將使用該操作符。
1.將派生類物件賦給基類物件
class Base
{};
class Derive
{};
int main()
{
Base a;
Derive d;
a = d;
}
上面的a=d;語句將使用誰的賦值操作符呢。
實際上,賦值語句將被轉換成左邊的物件呼叫的一個方法
a.operator=(d);
//左邊的為基類物件
簡而言之,可以將派生物件賦給基類物件,但這隻涉及到基類的成員。
2.基類物件賦給派生類物件
class Base
{};
class Derive
{};
int main()
{
Base a;
Derive d;
d = a;
}
上述賦值語句將被轉換為:
d.operator=(a);
//Derive::operator=(const Derive&)
左邊的物件為派生類物件,不過派生類引用不能自動引用基類物件,所以上述程式碼不能執行。或者執行出錯。
除非有下面的函式
Derive(const Base&)
{}
總結:
- 是否可以將基類物件賦給派生類物件,答案是也許。如果派生類包含了轉換建構函式,即對基類物件轉換為派生類物件進行了定義,則可以將基類物件賦給派生物件。
- 派生類物件可以賦給基類物件。
三、繼承與轉換——賦值相容規則(public繼承)
公有繼承條件下
派生類和基類之間的特殊關係為:
1.派生類物件可以使用基類的方法,條件是基類的方法不是私有的
2.基類指標可以在不進行顯示型別轉換的情況下指向派生類物件
3.基類引用可以再不進行顯示型別轉換的情況下引用派生類物件,但是基類指標或引用只能用於呼叫基類的方法,不能用基類指標或引用呼叫派生類的成員及方法
void FunTest(const Base&d)
{
}
void FunTest1(const Derive&d)
{
}
int main()
{
Derive d;
Base b(0);
b = d;//可以
d = b;//不行,訪問的時候會越界
//上面兩行程式碼在上一條中已經解釋過了
FunTest(b);
FunTest(d);
FunTest1(b); //不可以
FunTest1(d);
Base* pBase = &d;
Derive*pD = &b;//錯了
//如果非要這麼做只能通過強制型別轉換
Derive*pD = (Derive*)&b;//如果訪問越界,會崩潰
}
通常,C++要求引用和指標型別與賦給的型別匹配,但這一規則對繼承來說是個例外。但是這個例外是單向的,即僅僅不可以將基類物件和地址賦給派生類引用和指標。
如果允許基類引用隱式的引用派生類物件,則可以使用基類引用為派生類物件呼叫基類的方法,因為派生類繼承了基類的方法,所以這樣不會出現問題。
但是如果可以將基類物件賦給派生類引用,那麼派生類引用能夠為積累物件呼叫派生類方法,這樣做會出現問題,例如:用基類物件呼叫派生類中新增的方法,是沒有意義的,因為基類物件中根本沒有派生類的新增方法。