C++多态特性详解
目录
- C++多态特性详解
- 多态的定义和实现
- C++多态实现条件
- 虚函数重写的3个例外
- C++11新关键字:override,final
- 三个概念的对比
- 多态的底层实现原理
- 虚函数表
- 实现原理
- 单继承多态对象模型
- 多继承多态对象模型
- 菱形继承多态对象模型
- 菱形虚拟继承多态模型
- 抽象类
- 抽象类概念
- 抽象类的意义
C++多态特性详解
多态的定义和实现
不同的对象去做同一件事情,多种形态,结果不一样,就是多态。
C++多态实现条件
1.基类和派生类完成虚函数重写(三同:函数名,参数类型,返回值)
2.基类的指针或者引用取调用虚函数,使用对象不可以
最终结果:指向谁调用谁的虚函数
虚函数:用virtual修饰的成员函数被称为虚函数
示例:
#include<iostream>using namespace std;
class Person {
public:virtual void CheckIdentity() //使用virtual修饰,为虚函数{cout << " Is a person" << endl;}
private:int _a = 0;
};class Student : public Person
{virtual void CheckIdentity() //与person类中函数构成重写{cout << " Is a student" << endl;}
private:int _b = 1;
};int main()
{Person* p1 = new Person; //都是person类型的指针Person* p2 = new Student;//该函数符合虚函数重写条件,使用对象的指针调用即可实现多态效果p1->CheckIdentity(); //指向对象是person类型就调用person中的函数p2->CheckIdentity(); //指向对象是student类型就调用student中的函数delete p1;delete p2;return 0;
}
运行结果:
虚函数重写的3个例外
1.协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。返回值可以是另一对继承关系的指针或者引用。
#include<iostream>using namespace std;class A { int _a; };
class B : public A{};class Person {
public:virtual A* CheckIdentity() {cout << " Is a person" << endl;return nullptr;}int _a = 0;
};class Student : public Person
{virtual B* CheckIdentity() //与person类中函数构成重写{cout << " Is a student" << endl;return nullptr;}
};int main()
{Person* p1 = new Person; //都是person类型的指针Person* p2 = new Student;p1->CheckIdentity(); //指向对象是person类型就调用person中的函数p2->CheckIdentity(); //指向对象是student类型就调用student中的函数delete p1;delete p2;return 0;
}
运行结果:
可以看到,即使返回值类型不同,但是仍为父子类关系的指针或者引用,也可是实现多态
2.析构函数的重写
析构函数函数名不同,但是加上virtual之后还是可以构成重写,因为底层都将名字处理为destructor,这样可以做到指向谁调用谁的析构,可以解决下面的问题:
class Person {
public:~Person(){cout << "~Person" << endl;}
private:int _a = 0;
};class Student : public Person
{
public:Student():_b(new int[10]) //开辟空间{ }~Student(){delete[] _b; //释放空间cout << "~Student" << endl;}
private:int* _b;
};
int main()
{Person* p1 = new Person; //都是person类型的指针Person* p2 = new Student;delete p1;delete p2;return 0;
}
运行结果:
p2指向的对象为student类型的,其在堆上开了空间,在析构函数中进行释放,可是指针的类型是Person*,则去调用了Person的析构函数,没有进行空间的释放,导致了内存泄露。
而我们希望的是指向谁的空间就去调用谁的析构函数,因此我们给析构函数可以写成多态,这样就可以解决该问题。
代码示例:
class Person {
public:virtual ~Person(){cout << "~Person" << endl;}private:int _a = 0;
};class Student : public Person
{
public:Student():_b(new int[10]){ }virtual ~Student(){delete[] _b;cout << "~Student" << endl;}
private:int* _b;
};
int main()
{Person* p1 = new Person; //都是person类型的指针Person* p2 = new Student;delete p1;delete p2;return 0;
}
运行结果:
最后一行的调用~Person是因为在派生类析构结束时会自动的调用基类的析构
3.派生类重写虚函数省略virtual
在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议 这样使用
C++11新关键字:override,final
在多态的使用中,是否构成重写至关重要,而我们可能会因为疏忽或者其他问题导致函数无法构成重写,但是这种问题在编译期间是不会出错的,没有语法问题我们不容易发现,因此C++11提供了两个新的关键字:final, override,可以帮助我们进行检测是否构成了重写
用final修饰的函数表示不能被继承,用final修饰的类叫做最种类,也不能被继承
override,加载派生类函数声明后面,帮助我们检查有没有完成重写,没有重写则会报错
三个概念的对比
重载:在同一作用域的两个参数不同(类型,数量),函数名相同的函数,所构成的函数重载,通过识别参数来确认调用的那个函数
重写(覆盖):两个虚函数分别在基类和派生类的作用域中,函数名,参数类型,返回值都必须相同(除了上述的例外)。重写只是重新定义函数体实现,函数的结构部分(函数名返回值参数等)用的时父类的
重定义(隐藏):两个函数分别在基类和派生类的作用域,函数名相同,就构成重定义。两个基类和派生类的同名函数不构成重写就是定义。
多态的底层实现原理
虚函数表
在类内存在虚函数,所有的虚函数都会加入一张虚函数表中。同时编译器会在类内帮我们自动加一个指针成员,叫做虚函数表指针,简称为虚表指针,指向该类的虚函数表。
派生类的虚函数完成重写后,则会用重写后的函数的地址
同类型(基类或者派生类)共用一张虚表
class Person {
public:virtual void func1(){}virtual void func2(){}virtual ~Person(){cout << "~Person" << endl;}private:int _a = 0;
};class Student : public Person
{
public:virtual void func1(){}virtual void func2(){}Student() :_b(new int[10]){}virtual ~Student(){delete[] _b;cout << "~Student" << endl;}
private:int* _b;
};
int main()
{Person* p1 = new Person; //都是person类型的指针Person* p2 = new Student;delete p1;delete p2;return 0;
}
虚表存放的位置在哪里呢?
通过一段简单的代码就可以知道
int main()
{int i = 0;int* j = new int;static int t = 0;const char* k = "const";printf("栈区地址:%p\n", &i);printf("堆区地址:%p\n", &j);printf("常量区地址:%p\n", &k);printf("静态区地址:%p\n", &t);cout << endl;Person a;Student b;Person* p1 = &a;Student* p2 = &b;printf("person虚表地址:%p\n", *(int*)p1);printf("student虚表地址:%p\n", *(int*)p2);return 0;
}
我的编译器在静态区存放。对于虚函数表存放位置,C++标准并未规定,大家可以用该代码自行查看自己的在什么位置存放。
实现原理
如果调用函数满足虚函数重写,此时,基类和派生类就会各有一张虚表,派生类的虚表中存放着重写过的函数地址,在调用的时候去各自虚表找各自实现虚函数,从而实现多态(指向基类调用基类虚函数,指向派生类调用派生类重写的虚函数)。这种实现方式是在运行的时候去找函数地址,属于一种运行时多态。
没有满足虚函数重写,那么在编译的时候就已经根据调用该函数的指针类型确定了函数位置,普通调用也是如此(通过对象调用)
单继承多态对象模型
class Person {
public:virtual void func1(){cout << "Person->func1()" << endl;}virtual void func2(){cout << "Person->func2()" << endl;}private:int _a;
};class Student : public Person
{
public:virtual void func1() //重写{cout << "Student->func1()" << endl;}virtual void func3() //添加{cout << "Student->func3()" << endl;}virtual void func4() //添加{cout << "Student->func4()" << endl;}private:int _b;
};typedef void (*fp)();
typedef fp(*VFT)[];void PrintVFT(Person* ptr) //打印虚函数表
{printf("虚表地址:%p\n", *(void**)ptr);VFT VT = *(VFT*)ptr;int i = 0;while ((*VT)[i] != 0){printf("第%d个虚函数,虚函数地址为:%p ", i, (*VT)[i]);(*VT)[i]();i++;}cout << endl;
}
int main()
{Person a;Student b;PrintVFT(&a);PrintVFT(&b);return 0;}
派生类的虚表生成:
- 先将基类中的虚表内容拷贝一份到派生类虚表中
- 如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
- 派生类自己 新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
多继承多态对象模型
与单继承多态大部分相同,不同的是每个在派生类中每个基类都有属于自己的虚函数表,并且派生类所要添加的虚函数只添加在第一个继承的基类当中,第二个继承的基类虚表只进行重写,不进行添加
可以由以下代码验证:
class Person {
public:virtual void func1(){cout << "Person->func1()" << endl;}virtual void func2(){cout << "Person->func2()" << endl;}private:int _a;
};class Biologic {
public:virtual void func5(){cout << "Biologic->func5()" << endl;}virtual void func6(){cout << "Biologic->func6()" << endl;}private:int _b;
};class Student : public Person , public Biologic
{
public:virtual void func1() //重写{cout << "Student->func1()" << endl;}virtual void func3() //添加{cout << "Student->func3()" << endl;}virtual void func4() //添加{cout << "Student->func4()" << endl;}virtual void func6() //重写{cout << "Student->func6()" << endl;}private:int _c;
};typedef void (*fp)();
typedef fp(*VFT)[];void PrintVFT(void* ptr) //打印虚函数表
{VFT VT = *(VFT*)ptr;int i = 0;while ((*VT)[i] != 0){printf("第%d个虚函数,虚函数地址为:%p ", i, (*VT)[i]);(*VT)[i]();i++;}cout << endl;
}
void PrintPersonVFT(Person* ptr)
{printf("person虚表地址:%p\n", *(void**)ptr);PrintVFT(ptr);
}
void PrintBiologicVFT(Biologic* ptr)
{printf("Biologic虚表地址:%p\n", *(void**)ptr);PrintVFT(ptr);
}
int main()
{Person a;Biologic b;Student c;cout << "person a_______________________________" << endl;PrintPersonVFT(&a);cout << "Biologic b_______________________________" << endl;PrintBiologicVFT(&b);cout << "Student c_______________________________" << endl;PrintPersonVFT(&c);PrintBiologicVFT(&c);return 0;}
菱形继承多态对象模型
class A
{int _a;
public:virtual void func1(){cout << "A->func1()" << endl;}virtual void func2(){cout << "A->func2()" << endl;}};
class B : public A
{int _b;
public:virtual void func1(){cout << "B->func1()" << endl;}virtual void func3(){cout << "B->func3()" << endl;}};
class C : public A
{int _c;
public:virtual void func2(){cout << "C->func2()" << endl;}virtual void func4(){cout << "C->func4()" << endl;}};class D : public B, public C
{int _d;
public:virtual void func2(){cout << "D->func2()" << endl;}virtual void func5(){cout << "D->func5()" << endl;}
};typedef void (*fp)();
typedef fp(*VFT)[];void PrintVFT(void* ptr) //打印虚函数表
{VFT VT = *(VFT*)ptr;int i = 0;while ((*VT)[i] != 0){printf("第%d个虚函数,虚函数地址为:%p ", i+1, (*VT)[i]);(*VT)[i]();i++;}cout << endl;
}
void PrintAVFT(A* ptr)
{printf("A虚表地址:%p\n", *(void**)ptr);PrintVFT(ptr);
}
void PrintBVFT(B* ptr)
{printf("B虚表地址:%p\n", *(void**)ptr);PrintVFT(ptr);
}
void PrintCVFT(C* ptr)
{printf("C虚表地址:%p\n", *(void**)ptr);PrintVFT(ptr);
}int main()
{A a;B b;C c;D d;cout << " A a:_______________________________________________________________" << endl;PrintAVFT(&a);cout << " B b(class B : public A):____________________________________________" << endl;PrintBVFT(&b);cout << " C c(class C : public A):____________________________________________" << endl;PrintCVFT(&c);cout << " D d(class D : public B, public C):___________________________________" << endl;PrintBVFT(&d);PrintCVFT(&d);}
可以看到,菱形继承与多继承模型相同,只不过是BC两个类中还有各自继承的A类对象,在拷贝虚表的时候也一并拷贝。同时添加虚函数也添加到第一个继承的类的虚表中。
菱形虚拟继承多态模型
通过以下代码和内存监视,我们可以得到菱形虚拟继承的对象模型:
class A
{int _a = 1;
public:virtual void func1(){cout << "A->func1()" << endl;}virtual void func2(){cout << "A->func2()" << endl;}};
class B : virtual public A
{int _b = 2;
public:virtual void func1(){cout << "B->func1()" << endl;}virtual void func3(){cout << "B->func3()" << endl;}};
class C : virtual public A
{int _c = 3;
public:virtual void func2(){cout << "C->func2()" << endl;}virtual void func4(){cout << "C->func4()" << endl;}};class D : public B, public C
{int _d = 4;
public:virtual void func2(){cout << "D->func2()" << endl;}virtual void func5(){cout << "D->func5()" << endl;}
};int main()
{A a;B b;C c;D d;return 0;}
可以看到,在菱形虚拟继承中,只有一份基类A被共享,B,C页表中不在包含关于A中的虚函数,否则无法做到共享,此时,A就需要有自己的vptr虚表指针,供所有的派生类使用。
与此同时,虚拟继承的派生类还都具有虚基表指针vbptr,该指针指向的虚基表里存放着偏移量信息,前4个字节为虚表指针vptr的偏移量,为-4,后4个字节存放着A类对象相对虚基表指针的偏移量。
抽象类
抽象类概念
在虚函数函数体改为 = 0,称为纯虚函数
包含纯虚函数的类称为抽象类
抽象类不能实例化出对象
代码示例:
class Person {
public:virtual void func() = 0; // 纯虚函数,该类也是抽象类
};class Student : public Person //继承了Person,也拥有纯虚函数func,那么student也是抽象类
{};
int main()
{Person a; Student b;return 0;
}
抽象类的意义
如果有一个类继承了抽象类,那么这个派生类也不能实例化出对象,因为他继承了基类的纯虚函数,自身也成为了一个抽象类。
这就要求你要想示例化出派生类的对象,就必须进行纯虚函数的重写,让其不再是纯虚函数,在派生类中不存在纯虚函数后就可以进行实例化了。
某种程度上来说,抽象类间接强制派生类进行重写虚函数
class Person {
public:virtual void func() = 0; // 纯虚函数,该类也是抽象类
};class Student : public Person
{
public:virtual void func() //进行重写{cout << "Student rewrite func" << endl;}private:int _b;
};
int main()
{//Person a; //无法实例化,去掉注释就会报错Student b;Person* p = &b;p->func();return 0;
}