目录
多态
1. 多态的概念
编译时多态(静态多态):函数名相同,参数不同,参数的类型或者个数不同,返回值可以相同,可以不同。运行时编译(动态编译):函数名,参数,返回值都必须相同。
2. 多态的定义及实现
2.1 多态的构成条件
2.1.1 实现多态还有两个必须重要条件:
一、虚函数
定义:
- 虚函数是在基类中使用关键字
virtual
声明的成员函数。它允许在派生类中重写该函数,以实现多态性。- 当通过基类指针或引用调用虚函数时,实际调用的是派生类中重写的版本,而不是基类中的版本。
作用:
- 实现多态性:多态性使得程序可以根据对象的实际类型来决定调用哪个函数。这在处理不同类型的对象时非常有用,可以提高代码的灵活性和可扩展性。
- 例如,有一个图形类层次结构,包括基类
Shape
和派生类Circle
、Rectangle
等。可以在基类中声明一个虚函数draw
,然后在每个派生类中重写这个函数来实现不同形状的绘制。当使用基类指针指向不同的派生类对象并调用draw
函数时,会根据实际对象的类型调用相应的绘制函数。实现原理:
- C++ 通过虚函数表(Virtual Function Table,简称 vtable)来实现虚函数的多态性。每个包含虚函数的类都有一个指向虚函数表的指针,这个指针通常存储在对象的内存布局中。
- 虚函数表是一个函数指针数组,其中存储了该类中所有虚函数的地址。当创建派生类对象时,派生类的虚函数表会继承基类的虚函数表,并在需要重写的位置替换为派生类自己的函数地址。
- 当通过基类指针或引用调用虚函数时,实际上是通过对象的虚函数表指针找到对应的虚函数表,然后根据函数在表中的位置调用相应的函数。
二、虚继承
定义:
- 虚继承是一种在多重继承中避免二义性问题的技术。当一个类从多个基类继承,而这些基类又有共同的基类时,如果不使用虚继承,可能会导致共同基类在派生类中出现多个副本,从而引发二义性问题。
- 使用虚继承时,共同基类在派生类中只有一份实例,避免了数据冗余和二义性。
作用:
- 解决多重继承中的二义性问题:在复杂的类层次结构中,虚继承可以确保只有一个共同基类的副本存在于派生类中,从而避免了在访问共同基类成员时可能出现的二义性。
- 例如,有三个类
A
、B
和C
,其中B
和C
都继承自A
,如果再创建一个类D
同时继承自B
和C
,如果不使用虚继承,D
中会有两份A
的副本。当访问A
中的成员时,编译器无法确定应该使用哪个副本,从而导致二义性。使用虚继承可以确保D
中只有一份A
的实例。实现原理:
- 虚继承通过在派生类的对象内存布局中添加一个指向虚基类表(Virtual Base Class Table,简称 vbtable)的指针来实现。虚基类表中存储了虚基类在派生类对象中的偏移量等信息。
- 当创建派生类对象时,编译器会根据虚继承的关系调整对象的内存布局,确保虚基类的成员在派生类对象中的位置是唯一的。在访问虚基类成员时,通过虚基类表中的偏移量信息来找到正确的位置。
总之,虚函数和虚继承在 C++ 中都是非常重要的特性,它们分别用于实现多态性和解决多重继承中的二义性问题,能够提高程序的灵活性、可扩展性和可维护性。
2.1.2 虚函数
1 class Person
2 {
3 public:
4 virtual void BuyTicket() { cout << "买票-全价" << endl;}
5 };
小知识:基类的虚函数,一定要加virtual,才能进行重写;但是子类的虚函数 可以加virtual ,也可以不加。
2.1.3 虚函数的重写/覆盖
#include <iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket() { cout << "买票全价" << endl; }
};
class Student :public Person
{
public:
virtual void BuyTicket() { cout << "买票半价" << endl; }
};
void Func(Person* ptr)
{
//这里可以看到虽然都是Person指针Ptr在调用BuyTicket
//但是跟ptr没关系,而是由ptr指向的对象决定的
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
这里可以看出,多态实现的条件之一,必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向派⽣类对象;首先就是派生类,一定要继承基类。
class Student :public Person
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void talk() const
{}
};
class Dog :public Animal
{
public:
//虚函数
virtual void talk() const { cout << "汪汪" << endl; }
};
class Cat :public Animal
{
public:
//虚函数 构成多态
virtual void talk() const { cout << "(>^ω^<)喵" << endl; }
};
//普通函数,它接受一个Animal类的常量引用作为参数。
void letsHear(const Animal& animal)
{
animal.talk();
}
int main()
{
Cat cat;
Dog dog;
letsHear(cat);
letsHear(dog);
return 0;
}
2.1.4 多态场景的⼀个选择题
2.1.5 虚函数重写的⼀些其他问题
• 协变(了解)
#include <iostream>
using namespace std;
class A{};
class B:public A{};
class Person
{
public:
virtual A* BuyTicket()
{
cout << "买票-全价" << endl;
return nullptr;
}
};
class Student : public Person
{
public:
virtual B* BuyTicket()
{
cout << "买票-打折" << endl;
return nullptr;
}
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
• 析构函数的重写
p2
本身是一个指向A
类型的指针,但实际上它指向的是一个B
类型的对象。如果基类A
的析构函数不是虚函数,那么当使用delete p2
时,编译器只会根据指针的静态类型(这里是A*
)来决定调用哪个析构函数,即只会调用A
的析构函数,而不会调用实际对象类型(B
)的析构函数。
只有当基类的析构函数被声明为虚函数时,在运行时才会根据对象的实际类型来调用正确的析构函数,先调用派生类B
的析构函数,再调用基类A
的析构函数,从而确保正确地清理资源。
注意:这个问题⾯试中经常考察,⼤家⼀定要结合类似下⾯的样例才能讲清楚,为什么基类中的析构函数建议设计为虚函数。
#include <iostream>
using namespace std;
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B :public A
{
public:
~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;
}
先子后父,p2是指向派生类,那么就会先调用B类的析构函数,在调用A类的析构函数。
如果在子类和父类不构成多态的时候,那么他们的析构函数就会构成隐藏关系。虽然表面上,函数名不同,但他们的函数名就都会变成destructor,构成隐藏关系。在B类调用A类的析构函数就要A::~A()
2.1.6 override 和 final关键字
2.1.7 重载/重写/隐藏的对⽐
3. 纯虚函数和抽象类
- 由于基类
Car
中声明了纯虚函数Drive
,这使得Car
成为一个抽象类,不能直接实例化对象。但是可以通过指向派生类对象的基类指针来调用派生类中重写的虚函数,从而实现多态行为。 - 例如在
main
函数中,通过Car* pBenz = new Benz;
和Car* pBMW = new BMW;
创建了Benz
和BMW
两个派生类的对象,并将它们的地址赋给基类指针pBenz
和pBMW
。这样,在后续调用pBenz->Drive();
和pBMW->Drive();
时,会根据指针实际指向的对象类型(分别是Benz
和BMW
)来动态地调用相应派生类中的Drive
函数实现,展示出不同品牌汽车的不同驾驶特性(“Benz - 舒适” 和 “BMW - 操控”)。
#include <iostream>
using namespace std;
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()
{
//Car car; //会报错,纯虚函数不能实例化
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
return 0;
}
4. 多态的原理
4.1 虚函数表指针
4.2 多态的原理
4.2.1 多态是如何实现的
#include <iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
string _name;
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
string _id;
};
class Soldier :public Person
{
public:
virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:
string _codename;
};
void Func(Person* ptr)
{
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
ptr->BuyTicket();
}
int main()
{
// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后
// 多态也会发⽣在多个派⽣类之间。
Person ps;
Student st;
Soldier sr;
Func(&ps);
Func(&st);
Func(&sr);
return 0;
}
4.2.2 动态绑定与静态绑定
// 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
// BuyTicket不是虚函数,不满⾜多态条件。
// 这⾥就是静态绑定,编译器直接确定调⽤函数地址
ptr->BuyTicket();
00EA2C91 mov ecx, dword ptr[ptr]
00EA2C94 call Student::Student(0EA153Ch)
4.2.3 虚函数表
#include <iostream>
using namespace std;
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()
{
Base b;
Derive d;
return 0;
}
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;
}
运⾏结果:
栈 : 010FF954
静态区 : 0071D000
堆 : 0126D740
常量区 : 0071ABA4
Person虚表地址 : 0071AB44
Student虚表地址 : 0071AB84
虚函数地址 : 00711488
普通函数地址 : 007114BF
总结一下,对我自己的帮助挺大的,希望你也能有所收获~