C++中关于多态实现和使用方法
作者:止步听风
都说 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 类型
- 通过指向虚函数表的指针和偏移量,来匹配虚函数的地址
- 根据地址调用虚函数
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。