多态:指同一操作作用于不同对象时,可以产生不同的行为。 多态允许通过统一的接口处理不同类的对象,提高代码的灵活性和可扩展性。
举个例子: 比如父类有一个买票的函数,而对于买票这个行为,当普通成年人去买票时,买的是全价票;如果是一个学生去买票,买到的就是折扣票。所以至于最终买到的是什么票是取决于传递的是什么对象来决定的!
多态是⼀个继承关系的下的类对象,去调用同⼀函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象优惠买票。
#include<iostream>
using namespace std;
class Person
{
public:
void BuyTickt()
{
cout << "买票-全价" << endl;
}
};
class Student :public Person
{
public:
void BuyTickt()
{
cout << "买票-打折" << endl;
}
};
void Func(Person& p)
{
p.BuyTickt();
}
int main()
{
Person p;
Student s;
Func(p);//传一个Person对象
Func(s);//传一个Student对象
return 0;
}
我们想通过传入不同的对象,来得到不同的结果,但是发现与我们的预期不符。 原因是
Func函数接受Person&类型的参数,调用BuyTickt时根据参数的静态类型(Person)决定调用哪个方法。即使传入的是Student对象,调用的仍是基类Person的BuyTickt方法。由此说明该函数编译时就已经确定了函数的地址了,不由对象决定!因此可以发现他们是不构成多态的!!!
所以要想实现多态还必须要加上这两个重要的条件:
#include<iostream>
using namespace std;
class Person
{
public:
//在前面加上virtual变成虚函数
virtual void BuyTickt()
{
cout << "买票-全价" << endl;
}
};
class Student :public Person
{
public:
//在前面加上virtual变成虚函数
virtual void BuyTickt()
{
cout << "买票-打折" << endl;
}
};
void Func(Person& p)
{
p.BuyTickt();
}
int main()
{
Person p;
Student s;
Func(p);//传一个Person对象
Func(s);//传一个Student对象
return 0;
}
下面画一张图来解释一下,为什么加了这两个条件以后就构成多态了:

说明:
注意:
virtual修饰,那么这个成员函数被称为虚函数。非成员函数不能加virtual修饰!class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};class Person
{
public:
virtual void BuyTickt()
{
cout << "买票-全价" << endl;
}
};
class Student :public Person
{
public:
//=======================
//函数名,参数列表、返回值相同
//与基类的虚函数构成了重写
//=======================
virtual void BuyTickt()
{
cout << "买票-打折" << endl;
}
};
void Func(Person& p)
{
// 这⾥可以看到虽然都是Person的引用p在调⽤BuyTicket
// 但是跟p没关系,⽽是由p引用的对象决定的。
//这也就构成了运行时的多态
p.BuyTickt();
}注意: 在重写基类虚函数时,派生类的虚函数在
不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用!
下面的代码输出的结果是什么? A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}运行结果:

题目解析:

基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加
virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则。实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了vialtual修饰,派生类的析构函数就构成重写。
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A {
public:
//======================
//基类的析构加了virtual构成多态
//此时虽然名字不同,但编译器会特殊处理
//======================
~B()
{
//派生类重写/覆盖基类的析构函数
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
// 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能
// 构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0;
}
如果我们没有给基类的析构加上virtual,那么在delete p2的时候只会调用基类A的析构,而不会调用派生类B的析构,这就会导致内存泄露!!!

override关键字的作用: 类似于一个监视器的作用,它能显式地标记并监视派生类中重写基类虚函数的成员函数,如果没有重写或者函数名写错就会报错。final关键字的作用: 禁止类被继承或禁止虚函数被重写,如果一个类或一个虚函数不想被继承或重写那么就可以加上final关键字去修饰。//使用final修饰该类禁止继承!
class A final
{
public:
//使用final修饰禁止重写
virtual void func() final {}
};
class Car
{
public:
virtual void Dirve()
{
cout << "Dirve()" << endl;
}
};
class Benz :public Car
{
public:
//===========================
//基类函数Dirve与派生类Drive不同
//使用override就能检查出来!!!
//===========================
virtual void Drive override()
{
cout << "Benz-舒适" << endl;
}
};
关键区别示例:
void Print(int x) 和 void Print(string s) 共存。virtual void Show(),子类override void Show()。void Display(),子类new void Display()。纯虚函数: 纯虚函数是在基类中声明但没有实现的虚函数,其语法形式为在函数声明后添加 = 0;例如:
virtual void functionName() = 0;抽象类: 包含纯虚函数的基类叫做抽象类,抽象累不能示例化出对象,必须由派生类重写该函数后才能创建对象。
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
int main()
{
// 编译报错:error C2259: “Car”: ⽆法实例化抽象类
Car car;
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
return 0;
}总结一句话: 纯虚函数使得该类变成抽象类,抽象类强制派生类实现特定功能:确保所有继承自抽象类的子类都必须覆盖/重写纯虚函数,否则子类也会成为抽象类。
下⾯编译为32位程序的运⾏结果是什么() A. 编译报错 B. 运行报错 C. 8 D. 12
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
上面的运行结果是12(32位系统下),除了成员
_b和成员_ch,其实还有一个指针_vfptr存在该对象里面,这个指针称之为虚函数表指针。这个指针的大小刚好占4个字节所以根据内存对齐规则算得12。

注意: 每一个含有虚函数的类都要有一个虚表指针! 因为一个类所有虚函数的地址都要放到该对象的虚函数表中,该表也称虚表。该指针指向这张表,调用相应的虚函数的时候该指针就加上相应的偏移量找到对应函数然后完成调用!
回到我们一开始的那段代码:从底层的角度Func函数中p->BuyTicket(),是如何作为p指向Person对象调用Person::BuyTicket,p指向Student::BuyTickt的呢?

如上图: 满足多态条件后,底层不再是编译时通过调用对象来确定函数的地址,而是在运行时到执行的对象的虚函数表中确定对应的虚函数的地址。这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。
注意: 多态不仅仅发生在派生类对象之间!多个派生类继承基类,重写虚函数后,多态也会发生在多个派生类之间。
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
//=====================================
//当BuyTicket不是虚函数,不满⾜多态时。
//这⾥就是静态绑定,编译器直接确定调⽤函数地址!
//=====================================
ptr->BuyTicket();
00EA2C91 mov ecx,dword ptr [ptr]
00EA2C94 call Student::Student (0EA153Ch)通过上面的反汇编代码可以看到,当
Person与Student不构成多态时编译器直接确定调用(即直接call函数的地址),所以是静态绑定!
满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。
//=====================================
// ptr是指针+BuyTicket是虚函数满⾜多态条件。
// 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址
//=====================================
ptr->BuyTicket();
00EF2001 mov eax,dword ptr [ptr]
00EF2004 mov edx,dword ptr [eax]
00EF2006 mov esi,esp
00EF2008 mov ecx,dword ptr [ptr]
00EF200B mov eax,dword ptr [edx]
00EF200D call eax这里的两个寄存器
eax和edx形成两级间接寻址:eax先定位对象实例,edx再定位虚函数表,最后eax存储目标函数地址。然后再call eax最终执行间接调用,这个就是运行时确定函数的地址动态绑定!


class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};
class Derive : public Base
{
public:
// 重写基类的func1
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func1" << endl; }
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
int main()
{
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxxxx";
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
Base b;
Derive d;
Base* p3 = &b;
Derive* p4 = &d;
printf("Person虚表地址:%p\n", *(int*)p3);
printf("Student虚表地址:%p\n", *(int*)p4);
printf("虚函数地址:%p\n", &Base::func1);
printf("普通函数地址:%p\n", &Base::func5);
return 0;
}
本篇文章我们介绍了多态: 什么是多态? 同一操作作用于不同对象时,可以产生不同的行为。 多态的类型: 静态多态和动态多态。 多态的构成条件: 基类的成员函数必须是虚函数,必须是基类的指针或引用调用。 纯虚函数和抽象类: 在虚函数后面加=0,包含纯虚函数的类叫抽象类。 多态的原理: 运行时多态是通过虚函数表指针在运行时找到对应的虚函数从而到达多态的效果。