多态
多态定义
- C++中所谓的多态是指:由继承而产生的子类,对同一消息而产生不同的行为
多态举例代码
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
Person(string name):m_name(name)
{
}
virtual void reaction() {}
protected:
string m_name;
};
class StudentA :public Person
{
public:
StudentA(string name):Person(name)
{
}
virtual void reaction()
{
cout << m_name << "准备看书" << endl;
}
};
class StudentB :public Person
{
public:
StudentB(string name) :Person(name)
{
}
virtual void reaction()
{
cout << m_name << "准备写字" << endl;
}
};
class Stranger :public StudentA //孙子
{
public:
Stranger(string name):StudentA(name)
{}
virtual void reaction()
{
cout << m_name << "准备跑" << endl;
}
};
void TeacherComing(Person* p) //用父类指针去接子类地址
{
cout << "老师来了" << endl;
p->reaction(); //传哪个对象,调用哪个对象的reaction
}
int main()
{
StudentA s1("julian");
StudentB s2("kerr");
Stranger s3("mmmm");
TeacherComing(&s1);
TeacherComing(&s2);
TeacherComing(&s3);
return 0;
}
多态发生的三个必要条件
- 要有继承
- 父类有虚函数(virtual关键字),子类要有虚函数重写(重写是一模一样,不是重载,重载是不同作用域下:函数名相同,函数列表不同)
- 父类指针或引用指向子类对象(孙子也行)
注意:子类的虚函数只要是对父类的虚函数重写,子类重写的函数默认就是虚函数,前面加不加关键字virtual都无所谓,但是为了代码可读性,一般加上。如果子类加virtual,父类没加,基本没有任何意义(但是也存在于子类虚函数表内,如果父类指针指向子类对象,那么此函数无法得到,如果是子类指针指向孙类对象,则能够得到此虚函数),除非孙类重写虚函数,发生多态,那和父类没有关系了。
虚析构函数
举例:代码构造出如下图的类结构,A是父类,B是子类
class A
{
public:
A(const char* name)
{
cout << "A()" << endl;
int len = strlen(name);
pf = new char[len + 1];
strcpy_s(pf, len+1, name);
}
virtual void Fun()
{
cout << "A_Fun" << endl;
}
virtual ~A()
{
if (pf != NULL)
{
delete [] pf;
pf = NULL;
}
cout << "~A()" << endl;
}
protected:
char* pf;
};
class B :public A
{
public:
B(const char* str1, const char* name):A(str1)
{
cout << "B()" << endl;
int len = strlen(name);
ps = new char[len + 1];
strcpy_s(ps, len+1, name);
}
virtual void Fun()
{
cout << "B_Fun" << endl;
}
~B()
{
if (ps != NULL)
{
delete [] ps;
ps = NULL;
}
cout << "~B()" << endl;
}
private:
char* ps;
};
void Test(A* p) //此处发生多态
{
p->Fun(); //传谁调用谁的Fun(),因为Fun()是虚函数
delete p; //调用析构, 如果父类A的析构函数没有写成虚函数,则只调用 ~A()。
//因为参数p是A*指针,delete p只调用~A(),B的ps所指的空间泄露了
}
int main()
{
B* p=new B("julian", "kerr");
Test(p);
return 0;
}
结果:左边是父类是普通的析构函数, 右边是父类的析构函数写成虚函数的结果
结论:当设计的类具有多态现象的时候,析构函数尽量都写成虚析构函数,以防子类的析构函数没有调用
多态发生的原理
- 问题:为什么传递不同的子类地址给父类指针,调用的是子类各自的函数?
首先明白两个概念:
- VPTR(虚指针,指向虚函数表):只要类中申明有虚函数,默认自动添加的成员变量,32位:sizeof类的话自动加4字节
- 虚函数表(存放的是类的虚函数入口地址): 肯定是编译之后就不能修改的,一定存放在只读区域
仔细阅读以下代码:有父类,子类,孙类
class Father
{
public:
virtual void Fun(int a)
{
cout << "virtual Father Fun(int)" << endl;
}
void Fun(int a, int b)
{
cout << "Father Fun(int, int)" << endl;
}
private:
int a;
};
class Son : public Father
{
public:
virtual void Fun(int a)
{
cout << "virtual Son Fun(int)" << endl;
}
virtual void Fun(int a, int b)
{
cout << "Son Fun(int, int)" << endl;
}
};
class Grand_Son : public Son
{
public:
virtual void Fun(int a)
{
cout << "virtual G_Son Fun(int)" << endl;
}
void Fun(int a, int b)
{
cout << "virtual G_Son Fun(int, int)" << endl;
}
};
int main()
{
//
Son s;
Father* p = &s;
p->Fun(1); //virtual Son Fun(int)
p->Fun(1, 2); //Father Fun(int, int), 相当于继承,但是Son自己的Fun()无法访问
//
Grand_Son G_s;
Son* p1 = &G_s;
p1->Fun(1); //virtual G_Son Fun(int)
p1->Fun(1, 2); //virtual G_Son Fun(int,int)
return 0;
}
解释:每个具有虚函数的类,在创建的时候都默认有创建一个虚函数指针和虚函数表,当调用虚函数的时候先从虚函数表中去找(偏移量根据指针类型,如果是父类指针指向子类对象,那么虚函数表的偏移只能到父类虚函数表所有的),没有的话在去普通函数去找。如下图:
Son s;
Father* p = &s;
p->Fun(1);
p->Fun(1, 2); 不会在虚函数表里面找,尽管有,但是由于p是Father*类型,只能偏移到Fun(int),无法偏移到Fun(int,int),所以会在普通函数里面找(父类若没有提供,编译则报错)
结论:通过虚函数表指针去查表是在程序运行的时候进行的,普通函数在编译的时候就确定了调用的函数,所以虚函数的调用效率要低与普通函数。只需要将需要多态的函数申明为虚函数即可。
VPTR指针的分布初始化
- 是VPTR指针转变的过程(从父类转向子类)
#include "stdafx.h"
#include <iostream>
using namespace std;
class Father
{
public:
Father(int a)
{
this->a = a;
Print();
}
virtual void Print()
{
cout << "Father Print()" << endl;
}
private:
int a;
};
class Son : public Father
{
public:
Son(int a, int b):Father(a)
{
this->b = b;
}
virtual void Print()
{
cout << "Son Print()" << endl;
}
private:
int b;
};
int main()
{
Son s(1, 2);
return 0;
}
在上面的代码中, main函数创建Son s(1,2),//先调用Father构造,再调用子类的构造,在调用Father构造的时候,Print打印是Father还是Son?
结果:Father Print()
为什么调用的是父类Print(),在构造s的时候先调用Father的构造函数(红色部分),这时候VPTR当做Father类型,会临时指向Father的虚函数表;再调用Son的构造函数(蓝色部分),VPTR当做Son类型,将由指向Father虚函数表重新指向Son的虚函数表。
结论:虚函数指针VPTR是分布初始化的,所以不要在构造函数中写业务,也不要去调用虚函数,否则会可能出错。