C++语言学习(九)——多态
C++中所谓的多态(polymorphism)是指由继承而产生的相关的不同的类,其对象对同一消息会作出不同的响应。
多态性是面向对象程序设计的一个重要特征,能增加程序的灵活性。可以减轻系统升级,维护,调试的工作量和复杂度。
多态是一种不同层次分类下的重要联系,是一种跨层操作。
一、多态实现的前提
赋值兼容规则是指在需要基类对象的任何地方都可以使用公有派生类的对象来替代。
赋值兼容是一种默认行为,不需要任何的显式的转化步骤,只能发生在public继承方式中,是多态实现的前提条件。
赋值兼容规则中所指的替代包括以下的情况:
A、子类对象可以直接赋值给父类对象
B、子类对象可以直接初始化父类对象
C、父类引用可以直接引用子类对象
D、父类指针可以直接指向子类对象
当使用父类指针(引用)指向子对象时,子类对象退化为父类对象,只能访问父类中定义的成员,可以直接访问被子类覆盖的同名成员。
#include <iostream> using namespace std; class Parent { public: int m; Parent(int a) { m = a; } void print() { cout << "Parent m = " << m << endl; } }; class Child : public Parent { public: int m; Child(int a):Parent(a) { m = a; } void print() { cout << "Child m = " << m << endl; } }; int main() { Parent p(0); Child c(0); Parent p1(c); Parent* pp = &c;//指向子类对象的父类指针退化为父类对象 Parent& rp = c;//指向子类对象的父类引用退化为父类对象 pp->print(); //Parent m = 0 rp.print(); //Parent m = 0 //Child cc = static_cast<Child>(p);//需要转换构造函数支持 Child* pc = static_cast<Child*>(&p); pc->print();//Child m = xxxx return 0; }
在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。
父类也可以通过强转的方式转化为子类,但存在访问越界的风险。
子类中可以重定义父类中已经存在的成员函数,即函数重写。
当函数重写遇到赋值兼容时,编译器只能根据指针的类型判断所指向的对象,根据赋值兼容原则,编译器认为父类指针指向的是父类对象,只能调用父类中定义的同名函数。
二、多态形成的条件
根据父类指针指向的实际对象类型决定调用的函数,即多态。多态中,父类指针(引用)指向父类对象则调用父类中定义的函数,父类指针(引用)指向子类对象时调用子类对象的函数。
C++通过virtual关键字对多态进行支持,被virtual声明的函数被重写后具有多态属性。
多态形成的条件:
A、父类中有虚函数。
B、子类override(覆写)父类中的虚函数。
C、通过己被子类对象赋值的父类指针,调用共用接口。
1、虚函数
定义
class 类名
{
virtual 函数声明;
}
A、在基类中用 virual 声明成员函数为虚函数。类外实现虚函数时,不必再加 virtual。
B、在派生类中重新定义此函数称为覆写,要求函数名,返值类型,函数参数个数及类型全部匹配。并根据派生类的需要重新定义函数体。
C、当一个成员函数被声明为虚函数后,其派生类中完全相同的函数(显示的写出)也为虚函数。 可以在其前加 virtual 以示清晰。
D、定义一个指向基类对象的指针,并使其指向其子类的对象,通过该指针调用虚函数,此时调用的就是指针变量指向对象的同名函数。
E、构造函数不能为虚函数,在构造函数执行完毕后虚函数表指针才能被正确初始化。析构函数可以为虚函数,定义一个父类指针并使用new创建的子类对象初始化,使用delete释放父类指针的堆空间时,只会调用父类的析构函数,不会调用子类的析构函数,会造成内存泄漏,父类析构函数声明为虚函数可以避免这个问题。一般来说需要将析构函数声明为虚函数。构造函数执行时,虚函数表指针未被正确初始化,因此构造函数不可能发生多态;析构函数函数执行时,虚函数表指针已经被销毁,因此析构函数也不可能发生多态。构造函数和析构函数中只能调用当前类中定义的函数版本。
2、纯虚函数
定义
class 类名
{
virtual 函数声明 = 0;
}
A、含有纯虚函数的类,称为抽象基类,不可实列化。即不能创建对象,存在的意义就是被继承,提供族类的公共接口,java 中称为 interface。
B、纯虚函数只有声明,没有实现,被“初始化”为 0。
C、如果一个类中声明了纯虚函数,而在派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚函数,派生类仍然为纯虚基类。
3、虚函数的限制
A、只有类的成员函数才能声明为虚函数。
虚函数仅适用于有继承关系的类对象,所以普通函数不能声明为虚函数。
B、静态成员函数不能是虚函数
静态成员函数不受对象的捆绑,只有类的信息。
C、内联函数不能是虚函数
D、构造函数不能是虚函数
构造时,对象的创建尚未完成。构造完成后,才能算一个名符其实的对象。
E、析构函数可以是虚函数且通常声明为虚函数
F、含有虚函数的类,析构函数也必须声明为虚函数。在 delete父类指针的时候,会调用子类的析构函数。
三、多态应用实例
#include <iostream> using namespace std; class Parent { public: int m; Parent(int a) { m = a; } virtual void print() { cout << "Parent m = " << m << endl; } }; class Child : public Parent { public: int m; Child(int a):Parent(a) { m = a; } void print() { cout << "Child m = " << m << endl; } }; int main() { Parent p(0); Child c(0); Parent p1(c); Parent* pp = &c;//指向子类对象 Parent& rp = c;//指向子类对象 pp->print(); //Child m = 0 rp.print(); //Child m = 0 Child* pc = static_cast<Child*>(&p);//指向父类对象 pc->print();//Parent m = 0 return 0; }
四、C++对象模型分析
class是一种特殊的struct,class中的成员函数与成员变量是分开存放的,每个对象拥有独立的成员变量,所有的对象共享类中的成员函数。
1、类对象模型的内存排布
运行时对象退化为结构体的形式:
A、所有成员变量在内存中依次排布
B、由于内存对齐的存在,成员变量间可能存在内存间隙
C、可以通过内存地址访问成员变量
D、访问权限关键字在运行时失效
代码实例:
#include <iostream> #include <string> using namespace std; class A { int i; int j; char c; double d; public: void print() { cout << "i = " << i << ", " << "j = " << j << ", " << "c = " << c << ", " << "d = " << d << endl; } }; struct B { int i; int j; char c; double d; }; int main() { A a; cout << "sizeof(A) = " << sizeof(A) << endl; // 20 bytes cout << "sizeof(a) = " << sizeof(a) << endl; cout << "sizeof(B) = " << sizeof(B) << endl; // 20 bytes a.print(); B* p = reinterpret_cast<B*>(&a); p->i = 1; p->j = 2; p->c = ‘c‘; p->d = 3; a.print(); p->i = 100; p->j = 200; p->c = ‘C‘; p->d = 3.14; a.print(); return 0; }
2、类对象对成员函数的调用分析
类中的成员函数存在于代码段,调用成员函数时对象地址作为参数隐式传递给成员函数,成员函数通过对象地址隐式访问成员变量,C++语法隐藏了对象地址的传递过程。
3、继承对象模型
子类是由父类成员叠加子类成员得到的。
C++多态的实现原理:
当类中声明虚函数时,编译器会在类中生成一个虚函数表,用于存储virtual成员函数地址,虚函数表由编译器自动生成与维护,存在虚函数时每个对象中都有一个指向虚函数表的指针。
由于对象调用虚函数时会查询虚函数表,因此虚函数的调用效率比普通成员函数低。
对象中虚函数表指针存在对象开始空间中。
代码实例:
#include <iostream> #include <string> using namespace std; class Demo { protected: int mi; int mj; public: virtual void print() { cout << "mi = " << mi << ", " << "mj = " << mj << endl; } }; class Derived : public Demo { int mk; public: Derived(int i, int j, int k) { mi = i; mj = j; mk = k; } void print() { cout << "mi = " << mi << ", " << "mj = " << mj << ", " << "mk = " << mk << endl; } }; struct Test { void* p; int mi; int mj; int mk; }; int main() { cout << "sizeof(Demo) = " << sizeof(Demo) << endl; cout << "sizeof(Derived) = " << sizeof(Derived) << endl; Derived d(1, 2, 3); Test* p = reinterpret_cast<Test*>(&d); cout << "Before changing ..." << endl; d.print(); p->mi = 10; p->mj = 20; p->mk = 30; cout << "After changing ..." << endl; d.print(); return 0; }
五、抽象类与接口
面向对象的抽象类用于表示现实世界的抽象概念,是一种只能定义类型,不能产生对象的类,只能被继承并被重写相关函数,直接特征是相关函数没有完整实现。
C++语言没有抽象类的概念,通过纯虚函数实现抽象类。纯虚函数是指之定义原型的成员函数,C++中类如果存在纯虚函数就成为了抽象类。
抽象类只能用作父类被继承,子类必须实现父类纯虚函数的具体功能,如果子类没实现纯虚函数,子类也为抽象类。
抽象类不可以定义对象,但是可以定义指针,指针指向子类对象,当子类中实现了子类纯虚函数,可以实现多态。
C++中满足下列条件的类称为接口:
A、类中没有定义任何的成员变量
B、所有的成员函数都是公有的
C、所有的成员函数都是纯虚函数
从以上条件可以知道,接口是一种特殊的抽象类。
C++的接口代码实例如下:
#include <iostream> #include <string> using namespace std; class Channel { public: virtual bool open() = 0; virtual void close() = 0; virtual bool send(char* buf, int len) = 0; virtual int receive(char* buf, int len) = 0; }; int main() { return 0; }