都說 C++ 是面向對象的語言,其中的面向對象主要包括三部分:繼承,封裝,多態。繼承和封裝我們之前就簡單介紹過,這裏主要對多態的使用方法做一個簡單說明。
賦值兼容
賦值兼容說的是在使用基類對象的地方可以使用公有繼承類的對象來代替。賦值兼容是一種默認的行爲,不需要進行顯式轉換就能夠實現。
就比如在派生類拷貝構造函數的參數初始化列表中,我們會直接使用派生類對象作爲基類拷貝構造函數的參數,而不會報錯,這就是賦值兼容的表現。賦值兼容主要表現在:
- 派生類的對象可以賦值給基類對象
- 派生類的對象可以初始化基類的引用
- 派生類對象的地址可以賦值給指向基類的指針
- 但,發生賦值兼容之後,只能使用從基類繼承的成員
實例
#include <iostream>
using namespace std;
class PERSON
{
public:
PERSON(char *name_ = "***",char sex_ = '*')
:name(name_),sex(sex_){}
void display()
{
cout<<"The name is "<<name<<endl;
cout<<"The sex is "<<sex<<endl;
}
protected:
char *name;
char sex;
};
class STUDENT:public PERSON
{
public:
STUDENT(char *name_ = "***",char sex_ = '*',char *num_ = "100")
:PERSON(name_,sex_),num(num_){}
void display()
{
cout<<"The name is "<<name<<endl;
cout<<"The sex is "<<sex<<endl;
cout<<"The num is "<<num<<endl;
}
private:
char *num;
};
int main()
{
STUDENT st("zhangsan",'x',"100");
st.display();
PERSON per = st;
per.display();
PERSON &per2 = st;
per2.display();
PERSON *per3 = &st;
per3->display();
return 0;
}
結果爲:
The name is zhangsan
The sex is x
The num is 100
The name is zhangsan
The sex is x
The name is zhangsan
The sex is x
The name is zhangsan
The sex is x
上邊的程序可以看出,基類對象,引用和指針都可以使用派生類對象或者指針進行賦值,從而進行訪問。
其實也可以將基類指針強制轉換爲派生類指針,進行訪問,但這種形式絕不是賦值兼容:
int main()
{
PERSON per("zhangsan",'x');
per.display();
STUDENT *st = static_cast<STUDENT *>(&per);
st->display();
return 0;
}
結果爲:
The name is zhangsan
The sex is x
The name is zhangsan
The sex is x
The num is 夽@
上邊的程序中,是將基類的指針強制轉換派生類的指針,從而調用派生類的對象。
- 從實際上來說,該過程只是將以基類地址其實的一段內存交給了派生類的指針,因爲類對象只存儲數據成員,因此能夠對應訪問到從基類繼承到的數據成員。
- 但同時不確定原來基類成員後邊的空間有什麼東西,結果爲出現部分亂碼。
- 如果將基類和派生類位置對調就是賦值兼容了。
多態
C++ 中的多態主要說的是,在面向對象中,接口的多種不同的實現方式。
靜多態
C++ 中的多態是接口多種不同的實現方式。而我們之前提到過的函數重載也是接口的多種不同的實現方式,因此也可以稱之爲多態,只是函數重載是在編譯階段通過 name mangling 實現的,所以叫做靜多態。
動多態
而不在編譯階段而是在運行階段決定的多態就稱爲動多態。動多態的形成條件爲:
- 父類中有虛函數
- 子類 override 父類中的的虛函數
- 通過已被子類對象賦值的父類指針或引用,調用公有接口
格式
class classname
{
virtual datatype func(argu);
}
實例
#include <iostream>
using namespace std;
class PERSON
{
public:
PERSON(char *name_ = "***",char sex_ = '*')
:name(name_),sex(sex_){}
virtual void display()
{
cout<<"The name is "<<name<<endl;
cout<<"The sex is "<<sex<<endl;
}
protected:
char *name;
char sex;
};
class STUDENT:public PERSON
{
public:
STUDENT(char *name_ = "***",char sex_ = '*',char *num_ = "100")
:PERSON(name_,sex_),num(num_){}
virtual void display()
{
cout<<"The name is "<<name<<endl;
cout<<"The sex is "<<sex<<endl;
cout<<"The num is "<<num<<endl;
}
protected:
char *num;
};
class POSTGRADUATE:public STUDENT
{
public:
POSTGRADUATE(char *name_ = "***",char sex_ = '*',char *num_ = "***",char *job_ = "***")
:STUDENT(name_,sex_,num_),job(job_){}
virtual void display()
{
cout<<"The name is "<<name<<endl;
cout<<"The sex is "<<sex<<endl;
cout<<"The num is "<<num<<endl;
cout<<"The job is "<<job<<endl;
}
protected:
char *job;
};
int main()
{
POSTGRADUATE po("zhsangsan",'x',"100","paper");
po.display();
PERSON *per = &po;
per->display();
STUDENT *st = &po;
st->display();
return 0;
}
結果爲:
The name is zhsangsan
The sex is x
The num is 100
The job is paper
The name is zhsangsan
The sex is x
The num is 100
The job is paper
The name is zhsangsan
The sex is x
The num is 100
The job is paper
- 在基類中聲明虛函數時需要使用 virtual 關鍵字,在類外實現虛函數時,不用再加 virtual
- 在派生類中重新定義此函數的過程稱爲 override,此過程要求函數的要素全都不能發生改變,包括函數名,返回值類型,形參個數和類型,只有函數體可以改變
- 當基類中的函數成員被聲明爲 virtual 時,其派生類中完全相同的函數都會變爲虛函數,原則上派生類中的虛函數不用使用 virtual 關鍵字,但是爲了程序的可讀性,可以在派生類的對應函數前加上 virtual
- 定義一個指向基類的指針,並使其指向其子類對象的地址,通過該指針調用虛函數,此時調用的就是指針變量指向的對象
- 子類中 override 的函數,可以爲任意訪問類型
- 通過多態就避免了賦值兼容的問題
override
在虛函數的使用中,需要在派生類中 override 基類中的虛函數,表明該函數是從基類 override 得到的,override 的含義表明:
- override 的函數要素全都不能發生改變
- 包括函數名,返回值類型,形參個數和類型
- 只有函數體可以改變
而有時爲了可讀性,也爲了防止編寫時出錯,可以通過在函數後添加 override 關鍵字表明這是 override 得到的。如上邊的例子中:
virtual void display() override
使用上邊的形式可以嚴格語法書寫。
純虛函數
對於一些抽象基類來說,我們並不需要在其中的虛函數中編寫什麼語句,因此可以將之寫成純虛函數。
class classname
{
virtual datatype func(argu) = 0;
}
如上例所示,可以將 STUDENT 中的 display 函數定義爲純虛函數:
virtual void display() = 0;
只是此時不能夠調用 STUDENT 中的該函數了。
對於純虛函數而言:
- 含有純虛函數的類,稱爲抽象基類,不能夠創建該類對象,該類只能被繼承,提供公共接口
- 純虛函數的聲明形式就包含了聲明和實現
- 如果一個類中聲明瞭純虛函數,而在派生類中沒有定義該函數,則該虛函數在派生類中仍然是純虛函數,派生類仍然爲抽象基類,這意味着在第一次繼承的時候一定要定義該函數
- 從這個角度看,纔算是虛函數的正確用法,直接用來聲明爲純虛基類,從而被繼承
含有虛函數的析構函數
含有虛函數的類,析構函數也應該聲明爲虛函數。
#include <iostream>
using namespace std;
class PERSON
{
public:
PERSON(char *name_ = "***",char sex_ = '*')
:name(name_),sex(sex_){}
virtual void display()
{
cout<<"The name is "<<name<<endl;
cout<<"The sex is "<<sex<<endl;
}
~PERSON(){cout<<"PERSON"<<endl;}
protected:
char *name;
char sex;
};
class STUDENT:public PERSON
{
public:
STUDENT(char *name_ = "***",char sex_ = '*',char *num_ = "100")
:PERSON(name_,sex_),num(num_){}
virtual void display()
{
cout<<"The name is "<<name<<endl;
cout<<"The sex is "<<sex<<endl;
cout<<"The num is "<<num<<endl;
}
~STUDENT(){cout<<"STUDENT"<<endl;}
protected:
char *num;
};
int main()
{
{
STUDENT st("zhsangsan",'x',"100");
st.display();
}
cout<<"****************"<<endl;
PERSON *p = new STUDENT("zhsangsan",'x',"100");
p->display();
delete p;
return 0;
}
結果爲:
The name is zhsangsan
The sex is x
The num is 100
STUDENT
PERSON
****************
The name is zhsangsan
The sex is x
The num is 100
PERSON
此時如果將析構函數聲明爲 virtual:
virtual ~PERSON(){cout<<"PERSON"<<endl;}
結果爲:
The name is zhsangsan
The sex is x
The num is 100
STUDENT
PERSON
****************
The name is zhsangsan
The sex is x
The num is 100
STUDENT
PERSON
可以看出,對於堆對象來說,含有虛函數的類對象析構與棧對象析構是有所差別的。爲了防止這種情況出現,最好是將含有虛函數的析構函數聲明爲 virtual。
注意事項
- 因爲虛函數是用在繼承中的,因此只有類成員函數才能聲明爲虛函數
- 靜態成員函數不能是虛函數
- 內聯函數不能是虛函數
- 構造函數不能是虛函數
- 析構函數可以是虛函數且通常聲明爲虛函數
RTTI
(Run Time Type Identification,RTTI) 也叫運行時類型信息,也是通過多態實現的。
typeid
typeid 返回包含操作數數據類型信息的 type_info 對象的一個引用,信息中包括數據類型的名稱。要使用 typeid,需要在函數中包含:
#include <typeinfo>
- type_info 重載了操作符 ==,!= 用來進行比較
- 函數 name() 返回類型名稱
- type_info 的拷貝和賦值都是私有的,因此不可拷貝和賦值
#include <iostream>
#include <typeinfo>
using namespace std;
typedef void (*Func)();
class Base1
{
};
class Base2
{
public:
virtual ~Base2(){}
};
class Derive1:public Base1
{
};
class Derive2:public Base2
{
};
int main()
{
cout<<typeid(int).name()<<endl;
cout<<typeid(double).name()<<endl;
cout<<typeid(char *).name()<<endl;
cout<<typeid(char **).name()<<endl;
cout<<typeid(const char *).name()<<endl;
cout<<typeid(const char * const ).name()<<endl;
cout<<"********************"<<endl;
cout<<typeid(Func).name()<<endl;
cout<<typeid(Base1).name()<<endl;
cout<<typeid(Base2).name()<<endl;
cout<<typeid(Derive1).name()<<endl;
cout<<typeid(Derive2).name()<<endl;
cout<<"********************"<<endl;
Derive1 d;
Base1 &b = d;
cout<<typeid(b).name()<<endl;
cout<<typeid(d).name()<<endl;
cout<<"********************"<<endl;
Derive2 dd;
Base2 &bb = dd;
cout<<typeid(bb).name()<<endl;
cout<<typeid(dd).name()<<endl;
cout<<"********************"<<endl;
Base1 *p = &d;
cout<<typeid(p).name()<<endl;
cout<<typeid(*p).name()<<endl;
cout<<typeid(d).name()<<endl;
cout<<boolalpha<<(typeid(*p)== typeid(d))<<endl;
cout<<"********************"<<endl;
Base2 *pp = ⅆ
cout<<typeid(pp).name()<<endl;
cout<<typeid(*pp).name()<<endl;
cout<<typeid(dd).name()<<endl;
cout<<boolalpha<<(typeid(*pp)== typeid(dd))<<endl;
cout<<"********************"<<endl;
return 0;
}
結果爲:
i
d
Pc
PPc
PKc
PKc
********************
PFvvE
5Base1
5Base2
7Derive1
7Derive2
********************
5Base1
7Derive1
********************
7Derive2
7Derive2
********************
P5Base1
5Base1
7Derive1
false
********************
P5Base2
7Derive2
7Derive2
true
********************
從上邊可以看出,在 typeid 涉及到虛函數時,利用指針得到的結果就可能出現差別,因此在使用 typeid 時需要注意:
- 確保基類中至少定義了一個虛函數(虛析構也可)
- 在涉及到虛函數時,儘量不要將 typeid 應用於指針,而是應用於引用,或者解引用的指針
- typeid 是一個運算符,而不是函數
- typeid 運算符返回的 type_info 類型,其拷貝構造函數和賦值運算函數都聲明爲 private,因此不能用於 stl 容器。也因此我們一般不直接保存 type_info,而是保存 type_info 的 name 信息
Notice how the type that typeid considers for pointers is the pointer type itself(both a and b are of type class Base *). However, when typeid is applied to objects(like *a and *b) typeid yields their dynamic type (i.e. the type of their most derived complete object).
If the type typeid evaluates is a pointer preceded by the dereference operator (*), and this pointer has a null value, typeid throws a bad_typeid exception.
typecast
在之前的文章中,我們簡單介紹過 static_cast,reininterpreter_cast,const_cast 的用法,當時還剩下一個 dynamic_cast。
dynamic_cast 是一種運行時的類型轉換方式,因此用於運行時的轉換判斷。該轉換能夠檢查指針所指向的類型,然後判斷這一類型與轉換的目標類型是否相同,如果是返回對象地址,如果不是返回 NULL。
dynamic_cast 常用於多態繼承中,來判斷父類指針的真實指向。
#include <iostream>
#include <typeinfo>
using namespace std;
class A
{
public:
virtual ~A(){}
};
class B:public A
{
};
class C:public A
{
};
class D
{
};
int main()
{
B b;
A *pa = &b;
B *pb = dynamic_cast<B*>(pa); //成功
cout<<pb<<endl;
C *pc = dynamic_cast<C*>(pa); //成功 安全
cout<<pc<<endl;
D *pd = dynamic_cast<D*>(pa); //成功 安全
cout<<pd<<endl;
pb = static_cast<B*>(pa); //成功
cout<<pb<<endl;
pc = static_cast<C*>(pa); //成功 不安全
cout<<pc<<endl;
pb = reinterpret_cast<B*>(pa); //成功 不安全
cout<<pb<<endl;
pc = reinterpret_cast<C*>(pa); //成功 不安全
cout<<pc<<endl;
pd = reinterpret_cast<D*>(pa); //成功 不安全
cout<<pd<<endl;
return 0;
}
結果爲:
0x61fe8c
0
0
0x61fe8c
0x61fe8c
0x61fe8c
0x61fe8c
0x61fe8c
在上述幾種類型轉換中,dynamic_cast 的轉換用法算是比較安全的,因爲這種轉換方式是先比較再返回的,而 reininterpreter_cast 則是最不安全的,因爲這種轉換方式不做類型檢查直接將源類型重新解釋爲目標類型,容易出錯。
但 dynamic_cast 的目標類型必須是類的指針或者引用。
多態實現
虛函數
之前介紹函數重載,也就是靜多態是通過 name mangling 實現的,而 C++ 的動多態則是通過虛函數表(virtual table)實現的。這個表中主要是一個類的虛函數的地址表,這張表包含了繼承,override 的情況。在實際使用中,在含有虛函數的類對象中,該表會被分配到該對象的內存中,用於指明實際所要調用的函數。
C++ 編譯器保證虛函數表的指針存在於實例對象的最前面,這表示實例對象的地址就是該虛函數表的位置,然後就可以遍歷其中的函數指針,進行調用。
#include <iostream>
#include <typeinfo>
using namespace std;
class Base
{
public:
void f() { cout << "Base::f" << endl; }
void g() { cout << "Base::g" << endl; }
void h() { cout << "Base::h" << endl; }
private:
int data;
};
int main()
{
Base b;
cout<<"sizeof(Base) = "<<sizeof(Base)<<endl;
cout<<"sizeof(b) = "<<sizeof(b)<<endl;
return 0;
}
結果爲:
sizeof(Base) = 4
sizeof(b) = 4
如果基類中存在虛函數,則爲:
#include <iostream>
#include <typeinfo>
using namespace std;
class Base
{
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
private:
int data;
};
int main()
{
Base b;
cout<<"sizeof(Base) = "<<sizeof(Base)<<endl;
cout<<"sizeof(b) = "<<sizeof(b)<<endl;
return 0;
}
結果爲:
sizeof(Base) = 8
sizeof(b) = 8
可以看出有虛函數的基類的大小會比沒有虛函數的基類大小多出一個指針的大小。這個多出來的指針就是虛函數表的位置。
#include <iostream>
#include <typeinfo>
using namespace std;
class Base
{
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
private:
int data;
};
typedef void (*FUNC)(void);
int main()
{
Base b;
cout<<"sizeof(Base) = "<<sizeof(Base)<<endl;
cout<<"sizeof(b) = "<<sizeof(b)<<endl;
cout<<&b<<endl;
cout<<*((int **)*(int *)(&b)+0)<<endl;
cout<<*((int **)*(int *)(&b)+1)<<endl;
cout<<*((int **)*(int *)(&b)+2)<<endl;
cout<<*((int **)*(int *)(&b)+3)<<endl;
FUNC pf = NULL;
pf = (FUNC)*((int **)*(int *)(&b)+0);
pf();
pf = (FUNC)*((int **)*(int *)(&b)+1);
pf();
pf = (FUNC)*((int **)*(int *)(&b)+2);
pf();
return 0;
}
結果爲:
sizeof(Base) = 8
sizeof(b) = 8
0x61fe94
0x4029f0
0x402a24
0x402a58
0x3a434347
Base::f
Base::g
Base::h
上面的程序中:
- 先將 &b 轉換爲 int *(這樣能夠保證 +1 一次增加一個指針的大小),取得虛函數表的地址
- 然後,再次取址就得到了第一個虛函數的地址,也就是 Base::f
- 最後再轉換爲 int **,通過 +1,+2 後取址,就能夠得到 Base::g,Base::h
一般繼承(no override)
#include <iostream>
#include <typeinfo>
using namespace std;
class Base
{
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
private:
int data;
};
class Derive:public Base
{
virtual void f1() { cout << "Base::f1" << endl; }
virtual void g1() { cout << "Base::g1" << endl; }
virtual void h1() { cout << "Base::h1" << endl; }
};
typedef void (*FUNC)(void);
int main()
{
Derive b;
cout<<"sizeof(Base) = "<<sizeof(Base)<<endl;
cout<<"sizeof(Derive) = "<<sizeof(Derive)<<endl;
cout<<"sizeof(b) = "<<sizeof(b)<<endl;
cout<<&b<<endl;
cout<<*((int **)*(int *)(&b)+0)<<endl;
cout<<*((int **)*(int *)(&b)+1)<<endl;
cout<<*((int **)*(int *)(&b)+2)<<endl;
cout<<*((int **)*(int *)(&b)+3)<<endl;
cout<<*((int **)*(int *)(&b)+4)<<endl;
cout<<*((int **)*(int *)(&b)+5)<<endl;
cout<<*((int **)*(int *)(&b)+6)<<endl;
FUNC pf = NULL;
pf = (FUNC)*((int **)*(int *)(&b)+0);
pf();
pf = (FUNC)*((int **)*(int *)(&b)+1);
pf();
pf = (FUNC)*((int **)*(int *)(&b)+2);
pf();
pf = (FUNC)*((int **)*(int *)(&b)+3);
pf();
pf = (FUNC)*((int **)*(int *)(&b)+4);
pf();
pf = (FUNC)*((int **)*(int *)(&b)+5);
pf();
return 0;
}
結果爲:
sizeof(Base) = 8
sizeof(Derive) = 8
sizeof(b) = 8
0x61fe94
0x402ae0
0x402b14
0x402b48
0x402b94
0x402bc8
0x402bfc
0x3a434347
Base::f
Base::g
Base::h
Base::f1
Base::g1
Base::h1
在上邊的例子中,派生類沒有 override 任何父類的函數,並又重新定義了幾個虛函數,因此對於派生類來說:
- 虛函數按照其聲明順序在表中存放
- 父類的虛函數在子類的虛函數前邊
一般繼承(override)
#include <iostream>
#include <typeinfo>
using namespace std;
class Base
{
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
private:
int data;
};
class Derive:public Base
{
virtual void f() { cout << "Base::f1" << endl; }
virtual void g1() { cout << "Base::g1" << endl; }
virtual void h1() { cout << "Base::h1" << endl; }
};
typedef void (*FUNC)(void);
int main()
{
Derive b;
cout<<"sizeof(Base) = "<<sizeof(Base)<<endl;
cout<<"sizeof(Derive) = "<<sizeof(Derive)<<endl;
cout<<"sizeof(b) = "<<sizeof(b)<<endl;
cout<<&b<<endl;
cout<<*((int **)*(int *)(&b)+0)<<endl;
cout<<*((int **)*(int *)(&b)+1)<<endl;
cout<<*((int **)*(int *)(&b)+2)<<endl;
cout<<*((int **)*(int *)(&b)+3)<<endl;
cout<<*((int **)*(int *)(&b)+4)<<endl;
cout<<*((int **)*(int *)(&b)+5)<<endl;
FUNC pf = NULL;
pf = (FUNC)*((int **)*(int *)(&b)+0);
pf();
pf = (FUNC)*((int **)*(int *)(&b)+1);
pf();
pf = (FUNC)*((int **)*(int *)(&b)+2);
pf();
pf = (FUNC)*((int **)*(int *)(&b)+3);
pf();
pf = (FUNC)*((int **)*(int *)(&b)+4);
pf();
return 0;
}
結果爲:
sizeof(Base) = 8
sizeof(Derive) = 8
sizeof(b) = 8
0x61fe94
0x402b54
0x402ad4
0x402b08
0x402b88
0x402bbc
0x3a434347
Base::f1
Base::g
Base::h
Base::g1
Base::h1
在上邊的例子中,派生類 override 了父類的 f 函數,並又重新定義了幾個虛函數,因此對於派生類來說:
- override 的 f 函數被放到了虛函數表中原來基類虛函數的位置
- 沒有 override 的函數不變
過程推斷
Base *b = new Derive();
b->f();
這段代碼的實際過程爲:
- 明確 b 類型
- 通過指向虛函數表的指針和偏移量,來匹配虛函數的地址
- 根據地址調用虛函數