通俗的说就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
生活中的多态例如:不同的手机抢一个红包,金额是不同的;同一家店分为不同种的会员等等
大致可以分为静态的多态和动态的多态
静态的多态:如函数重载
动态的多态是本章着重介绍的
动态多态的定义:一个父类的指针或引用,去调用(父类或子类)同一个函数,根据传递的类不同,调用结果也不同
多态的必要条件:
1.父类的指针或引用去调用
2.子类(派生类)必须构成函数重写
函数重写:
(1).子类和父类必须构成虚函数(在函数名前加 virtual 且修饰的是非静态成员函数)
(2).父类和子类的函数必须构成三同:即返回值相同、参数相同、函数名相同
代码示例如下
class Person
{
public:
virtual void BuyTicket()//虚函数:virtual修饰的非静态成员函数
{
cout << "买票 - 全价" << endl;
}
};
class Student : public Person
{
public:
//子类中满足:必须是虚函数,且函数名、参数、返回值与父类的都相同时就构成重写(覆盖)
virtual void BuyTicket()
{
cout << "买票 - 半价" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
Func(p);
Func(s);
return 0;
}
只要以上条件少一个,就不满足多态了
假如接收传递的不是父类的指针或引用那会有两种情况:
1.若是父类的普通对象接收传递的类的,那么调用的函数一定是父类自己的函数。因为当接收传递的是父类时就是父类本身,接收传递的是子类时会发生切片,把父类的部分切给形参接收。
所以无论怎么样,明确显示了形参的类型就是父类,且传递的子类还会发生切片,所以一定调用的是父类的函数。
2.若是子类接收传递的类,那么无论是指针引用还是普通变量,都只能接收子类,因为接收父类不能切片,会存在子类的访问越界的风险;
在默认情况下调用的是子类的函数,因为形参类型是子类且构成函数隐藏,想要调用就需要指定类域
上述是标准的定义方式及多态需要满足的条件,但有个例外情况
协变:若返回值是父子关系的指针或引用,那么也可以构成多态
代码如下:
class A
{};
class B:public A
{};
class Person
{
public:
//virtual void BuyTicket()
//virtual A* BuyTicket()
virtual Person* BuyTicket()
{
cout << "买票 - 全价" << endl;
return nullptr;
}
};
class Student : public Person
{
public:
//virtual void BuyTicket()
//virtual B* BuyTicket()
virtual Student* BuyTicket()
{
cout << "买票 - 半价" << endl;
return nullptr;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
Func(p);
Func(s);
return 0;
}
析构函数在编译时会把所以析构函数名替换为 destructor(),把三同问题中,唯一函数名不同的问题解决了,只需要加上virtual 就可以构成函数重写
但是要构成析构函数的多态比较复杂,下面分三种情况介绍
class Person
{
public:
//virtual ~Person()
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
//virtual ~Student()
~Student()
{
cout << "~Student()" << endl;
}
};
1.普通对象的析构
Person p;
Student s;
这种类型的对象,无论是否构成多态,都会正确调用析构函数的
父类调用父类自己的;子类调用子类自己,子类中父类在子类结束作用域后自动调用父类自己的(栈帧中的先定义后析构的问题在上一节继承中有介绍)
2.动态申请对象时,父类指针接收动态内存地址
(1)调用的是父类指针,但不构成多态会发生什么?
先说结论,不构成多态会造成内存泄漏,原因如下:
Person* pp = new Person;
Person* ps = new Student;
delete pp;
delete ps;
首先pp接受的时父类类型申请的动态内存,new的顺序是 :operator new + Person构造函数
当他析构时,delete的是pp这个变量,这个变量是Person类型,则调用顺序是:PP的类型的 析构函数 + operator delete,( pp->destructor() ,pp是属于person作用域的) 。
那么这里析构函数调用的就是Person自己的 ~Person()
而Student new的顺序是:先是operator new + Person的构造函数 之后是 operator new + Student的构造函数
到这里都还是正常的,但是当delete ps时,就出问题了
析构时的顺序:ps类型的 析构函数 + operator delete ,而ps类型是Person,那么调用的就是父类的析构函数+delete(Person作用于的ps->destructor(),以及Person的delete)
所以会造成内存泄漏
(2)调用的是父类的指针,且构成多态(满足函数重写)
在析构函数前加上virtual 即可
3.动态申请对象时,子类指针接收动态内存地址
首先子类不能接收 父类类型申请的动态内存地址,因为会发生越界等意想不到的问题
那么就是子类指针接收子类对象
Student* ps = new Student;
delete ps;
这时不会构成多态,并且会正确调用析构函数的,因为ps所属类型是Student的,那么构造时就会先构造继承的Person部分,再构造Student自己的;析构时会先调用Student自己的,继承中父类部分发现所在子类的作用域结束了,会自动调用Person的析构
(编译时会发现调用两个destructor(),但所属作用域是不同的)
总结:
1.普通类型,无论是否构成多态都会正确调用
2.动态申请时,若不构成多态,则需要申请类型和指针的类型保持一致(否则父类的指针接收子类的内存时会在析构时发生内存错误),若构成多态,则会正确调用
也就是指针指向什么类型的动态内存,就调用什么类型的destructor()
由于析构函数反映出的问题,可能是为了防止实际工程中发生内存错误,C++增加了一个例外,析构函数构成多态的条件:
父类必须写virtual,子类可以不用写,会继承父类中虚函数的属性,子类中同样的函数也就有了虚函数属性,就构成了子类(派生类)虚函数重写
大佬设计初衷可能是,防止 当父类加上virtual 构成虚函数,但是犹豫一些疏漏等原因并没有构成多态,没调用到子类的析构函数,就会造成内存泄漏的场景。但同时也衍生出一些小问题,代码如下:
class Person
{
public:
//例外:父类必须写virtual ,子类可以不用写,因为他把属性继承下来了
virtual void BuyTicket()
{
cout << "买票 - 全价" << endl;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
//virtual void BuyTicket()
void BuyTicket()//例外: 虽然子类 没写virtual ,但是他把父类的虚函数属性继承下来,所以也算虚函数
{
cout << "买票 - 半价" << endl;
}
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
Func(p);
Func(s);
return 0;
}
由于可以允许 父类必须写virtual而 子类可以不写virtual,又衍生出一个例外:
这也是因为,子类继承了父类所的虚函数所有的属性,父类这里的函数是公有的,所以到了子类这里就是公有了
建议自己写的时候按标准规范来写,肯定不会出错,代码想表达的意思还明确
假如想要类不能被继承,该如何实现?
方法1:
class A
{
private:
A(int a = 0)
:_a(a)
{}
public:
static A CreateOBJ(int a = 0)
{
//new
return A(a);
}
protected:
int _a;
};
class B : public A
{
};
把父类的构造函数设置成私有,当子类实例化对象时,先会调用父类的构造函数,但这里把他设置为私有就掉不成了,达到了不能继承的目的,但这种方法比较麻烦,代码可读性也不好
方法2:final关键字(C++11中新增加的)
class A final //不可被继承 C++11
{
private:
A(int a = 0)
:_a(a)
{}
public:
static A CreateOBJ(int a = 0)
{
//new
return A(a);
}
virtual void f()final
{
cout << "f()" << endl;
}
protected:
int _a;
};
class B : public A
{
public:
virtual void f()
{
cout << "f()" << endl;
}
};
在父类名后面加final,代表该类不能被继承
在父类中的虚函数后面加final,代表该虚函数不能被继承
override关键字的使用方式如下:
class A
{
public:
virtual void f()
{
cout << "f()" << endl;
}
protected:
int _a;
};
class B : public A
{
public:
virtual void f()override
{
cout << "f()" << endl;
}
}
加在派生类虚函数后面,检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
定义方式:
class flower//抽象类用于定义生活中 无法具体化的事物
{
public:
virtual void kind() = 0; //纯虚函数 实现没意义,因为他不可实例化
/*virtual void kind() = 0
{
cout << "种类:" << endl;
}*/
void func()
{
cout << "func" << endl;
}
};
class rose :public flower
{
public:
virtual void kind()
{
cout << "种类:玫瑰" << endl;
}
};
int main()
{
//flower* sp = new flower;
flower* p = new rose;
p->kind();
p->func();
}
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
由于抽象类不能实例化出对象,也就调不到他的函数,就算实现出功能也没有意义,所以一般抽象类只声明不实现。
先看一段代码:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b ;
};
int main()
{
Base b;
cout << sizeof(b) << endl;
}
若按普通类计算应该为:int _b 0 ~ 3 内存对齐数为最大对齐数是4 所以应该共占4字节
而实际情况是,除了int _b还有一个虚函数指针_vfpr(我这里是32位的)占4字节,0-3 + 4-7 共8字节并且满足内存对齐
这里用最开始的全价票半价票的代码来解释一下
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票 - 全价" << endl;
}
protected:
int _a;
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票 - 半价" << endl;
}
protected:
int _b;
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
Func(p);
Func(s);
return 0;
}
虚表指针是指向虚表的,而虚函数表本质是一个存虚函数的指针数组
再来解释一下为什么只能是父类的指针或引用调用
首先必须是父类,假如若是子类,就只能接收子类的对象了,因为他不能对父类进行反向切片,会越界
其次是指针或引用,访问的是传递进来的对象本身的父类部分,而传值传参是一个临时对象,实例化对象本身在编译时就确定了函数地址了
Person p1 = s;
Person& p2 = s;
再例如:假如父类对象可以把子类的虚表切下来保存,那么析构的时候就会混乱了
并且:
不满足多态时,编译时确定地址
满足多态时,会在运行时到虚表中去查找再调用
所以普通对象调用速度相对较快
注意:虚表中的虚函数指针只是指向虚函数的地址,而虚函数和类中普通函数一样,都是存在公共区(代码段)这里说的只是vs环境下的情况
这里就合理解释了前面的例外情况2,为什么子类私有的虚函数也可以调用,因为他们是去虚函数表了去查找调用虚函数,虚函数表分不出分私有公有,并且调用的接口属于父类,父类中是公有
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d;
};
int main()
{
Base b;
Derive d;
return 0;
}
子类继承时把父类的虚函数表拷贝了一份,之后子类的func1重写后把地址覆盖到原来func1的位置上
由于这个示例是在vs环境上演示的,子类能显示出来的虚函数表有多少个虚函数取决于父类的虚函数表有多少个虚函数。实际子类虚函数的数量不一定和父类对应 (如上述代码)
通过前面的 一些调试截图不难发现:
1.对象中第一个变量是指针(虚表指针),指针大小占4字节(64位8字节),
2.虚表实际上是一个函数指针数组
所以可以利用这两点来达到打印虚表的目的,具体代码如下:
class Base
{
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
public:
private:
int _b;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d;
};
typedef void(*VF_PTR)(); //VF_PTR代表 函数指针
void print(VF_PTR* table) //函数指针数组 VF_PTR table[] 等价,只是做参数时数组退化成指针,形参接的是首元素地址
{
虚表指针存的是虚表首元素地址,首元素又是个虚函数指针,所以这里可以也理解为二级指针
for(int i = 0;table[i] != nullptr;i++)
{
printf("vft[%d] : %p -> ",i,table[i]);
VF_PTR f = table[i];
f();
}
}
int main()
{
Base b;
Derive d;
print( (VF_PTR*)(*(int*)&b) );
cout << endl;
print( (VF_PTR*)(*(int*)&d) );
虚表指针是对象b的首元素,也就是前4个字节,所以这里强转一下int接收再解引用,
对象b就变成int b了,这时再次转成函数指针数组就可以了
return 0;
}
子类是把父类的虚表拷贝了 一份,并把子类重写的虚函数覆盖到原先父类虚函数在虚表的位置上
在vs上的监视窗口并没有显示全,显示的元素个数只取决于父类虚表的个数,也算是一个小bug
利用虚表调用虚函数无论是私有还是公有,都可以调的到,说明虚表是不分私有公有的,但是常规对象调用时,是不可以调得到的
class Base1
{
public:
virtual void func1() {cout << "Base1::func1" << endl;}
virtual void func2() {cout << "Base1::func2" << endl;}
private:
int b1;
};
class Base2
{
public:
virtual void func1() {cout << "Base2::func1" << endl;}
virtual void func2() {cout << "Base2::func2" << endl;}
private:
int b2;
};
class Derive : public Base1, public Base2
{
public:
virtual void func1() {cout << "Derive::func1" << endl;}
virtual void func3() {cout << "Derive::func3" << endl;}
private:
int d1;
};
int main()
{
Derive d;
print( (VF_PTR*)(*(void**)&d) );
cout << endl;
//Base2 b2;
//print( (VF_PTR*)( *(void**)( (char*)&d + sizeof(Base1) ) ));
Base2* p = &d;
print( (VF_PTR*)( *(void**)p ) );
或者
print( (VF_PTR*)*(Base2**)p );
解引用是取到虚表指针本身,因为要传递的是虚表指针的值
return 0;
}
通过打印虚表可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
注意:
多继承base1是先继承的,所以base1的虚表指针是在整体对象的前4个字节,比较好取出,而Base2的虚表指针需要找到Base2的首地址,在取出前4个字节才是Base2的虚表指针
同时在取字节数时不要忘了分清当前系统位数,例如32位指针类型占4字节,正好与前4个字节对应
这里来解释一下为什么要强转二级指针再解引用,而直接传p就不行。明白原理的请略过
实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。如果好奇心比较强的佬们,可以去看下面的两篇链接文章。