目录
一,什么是多态?
通俗来说,就是不同的对象去完成某个行为时,会产生出不同的形态,为不同的数据类型的对象提供统一的接口
举个例子:买票时,普通人买票时全价购买,学生买票是半价,军人买票时是优先买票。
二,多态的定义和实现
2.1虚函数
在类中,被virtual关键字修饰的成员函数称为虚函数
class Person1
{
public:
//虚函数
virtual void BuyTicket()
{
cout << "买票全价" << endl;
}
};
2.2重写
重写也称为覆盖,当派生类有一个函数名,函数参数,返回值和基类对应的函数都相同的时候,称为派生类虚函数重写了基类的虚函数。(如果函数参数不同但是没有使用,也不构成重写)
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票全价" << endl;
}
};
class Student : public Person
{
public:
//这里叫做虚函数的额重写,函数名参数和返回值都要求相同,如果参数不同但是没有使用参数,也不构成重写
//如果不符合重写就是隐藏关系
virtual void BuyTicket()
{
cout << "买票半价" << endl;
}
};
注:如果基类虚函数加了virtual而派生类虚函数没加,依然可以构成重写,因为基类虚函数被继承下来后,派生类依然保留了基类虚函数的特性,但是为了规范,建议子类虚函数也加上virtual。
2.3多态实现的条件和引用场景
1,必须通过基类的指针或者引用调用虚函数。
2,被调用的函数必须是虚函数,且派生类必须对基类虚函数进行重写。
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()//完成虚函数重写
{
cout << "买票半价" << endl;
}
};
//此处完成对基类虚函数的引用调用
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Func(p);
Student s;
Func(s);
return 0;
}
如果不通过引用或者指针调用,就变成下面这样
2.4虚函数重写的两个例外
2.4.1协变
派生类重写虚函数时,与基类虚函数返回值类型不同,如下代码,依然也构成重写。
class Person
{
public:
//基类虚函数返回基类对象的指针或引用
virtual Person& BuyTicket()
{
return *this;
}
};
class Student : public Person
{
public:
//派生类返回派生类对象的指针或引用
virtual Student& BuyTicket()
{
return *this;
}
};
2.4.2析构函数的重写
当基类的析构函数为虚函数时,此时派生类析构函数只要定义,无论是否加virtual关键字,无论名字是否相同,都与基类的析构函数构成重写。函数名不同看起来违背了重写的规则,但正如小标题所言,这是重写的一个特例。之所以有这个特例,是因为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
class Person {
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person {
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
三,抽象类
在虚函数后面加上“=0”,这个虚函数被称为纯虚函数,包含纯虚函数的类叫做抽象类,也叫做接口类,抽象类不能实例化出对象,并且被派生类继承后也不能实例化出对象。只有重写虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写虚函数,纯虚函数也体现了接口继承
class Car
{
public:
virtual void Drive() = 0;
};
class BMW : public Car
{
public:
virtual void Drive()
{
cout << "操控-好开" << endl;
}
};
class Benz : public Car
{
public:
virtual void Drive()
{
cout << "环境-舒适" << endl;
}
};
int main5()
{
//Car c;
//BMW b;//纯虚函数不能实例化出对象,父类子类都不能
Car* ptr = new BMW;
ptr->Drive();
ptr = new Benz;
ptr->Drive();
return 0;
}
class A
{
public:
virtual void func(int val = 1){ cout<<"A->"<< val <<endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val=0){ cout<<"B->"<< val <<endl; }
};
int main()
{
B*p = new B;
p->test();
return 0;
}
四,多态的原理
4.1虚函数表
4.1.1虚函数表是什么?
注:下面打印虚函数的操作都需要在32位下才能运行
上图是一个普通的虚函数创建和实例化对象,查看对象的地址后可以发现,除了_a有自己的地址外,还有一个_vfptr的东西。
这个_vfptr默认创建在类前面的位置,我们叫做虚函数表指针,简称虚表,咱们在类里面创建的虚函数的地址都存在这个东西里面,由于_vgptr存的都是指针,所以本质是一个指针数组。
4.1.2虚函数表里有什么?
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person::买票-全价" << endl;
}
virtual void Func1()
{
cout << "Person::Func1()" << endl;
}
};
//只要是虚函数都会存虚表里去
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "Student::买票-半价" << endl;
}
virtual void Func2()
{
cout << "Student::Func2()" << endl;
}
};
可以看到,不同对象有着自己的虚表,调用时也会去自己的虚表里调用自己实现的虚函数,这样就实现了不同对象完成同一行为时,展现不同的形态。
4.1.3打印虚表
从上图也可以发现BuyTicket和Func1都存进虚表里了,但是没有显示Func2,其实这里不是没有存进去,只是VS在这里做了点处理,导致Func2没有显示。
那我们可以通过下面的函数打印虚表
typedef void(*VFPTR) ();//指针数组的typedef
void PrintVFTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :%p,->", i, vTable[i]);
vTable[i]();//直接调用指针数组中指针指向的函数
}
cout << endl;
}
int main()
{
Person p;
Student s;
PrintVFTable((VFPTR*)*(int*)&p);//先把地址搞成四个,然后强转int得到四个字符,然后再强转
PrintVFTable((VFPTR*)*(int*)&s);
return 0;
}
上图可以看出,Func2确实是有被放进虚表里。
4.2多继承的虚函数表
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1 = 1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2 = 2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d = 3;
};
typedef void(*VFPTR) ();//指针数组的typedef
void PrintVFTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i]!=nullptr; ++i)
{
printf(" 第%d个虚函数地址 :%p,->", i, vTable[i]);
vTable[i]();//直接调用指针数组中指针指向的函数
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVFTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
PrintVFTable(vTableb2);
return 0;
}
五,补充
1,重载,重写,重定义。
重载:两个函数在同一作用域,并且函数名和参数(类型,顺序,个数)要不同。
重写(覆盖):两个函数分别在基类和派生类两个作用域,且函数名,参数,返回值也必须相同,而且必须是虚函数。
重定义(隐藏):两个函数分别在基类和派生类两个作用域,函数名相同的情况下,如果不构成重写那就是构成重定义。
2,静态成员不能是虚函数,因为静态成员没有this指针,使用访问作用域操作符“::”无法访问虚函数表,所以静态成员无法放进虚函数表。
3,构造函数不能是虚函数,因为对象创建虚表就是在构造函数初始化列表的时候创建的。
4,析构函数可以是虚函数,且最好把基类的析构函数定义成虚函数。
5,虚函数表在什么阶段生成,存在哪里?
如下代码:
class A
{
public:
virtual void func1(){ cout << "A::func1()" << endl; }
};
class B : public A {};
int main()
{
B b;
printf("虚表:%p\n", *((int*)&b));//可以看出虚表存在常量区
static int x = 0;
printf("static变量:%p\n", &x);
const char* ptr = "hello world";
printf("常量:%p\n", ptr);
int y = 0;
printf("局部变量:%p\n", &y);
printf("new变量:%p\n", new int);
return 0;
}
可以看出,虚表存在常量区