目录
多态的理解
多态:当不同的对象在做同一件事情的时候会做出不一样的动作且产生不同的结果(同样的函数不同的实现方式)
举个很形象的例子,一个函数叫上课,但是张三的实现是认真记笔记,而李四的实现是在教材上画小人,王五的实现则是跟赵六聊天,而这就是多态的表现形式。
多态的定义:不同继承关系的类对象,去调用同一函数,产生了不同的结果。关于继承的知识可以翻阅我过往的文章。
1. 虚函数
被virtual关键字修饰的类的非静态成员函数称为虚函数
【示例】
class Person
{
public:
// 虚函数
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
注意:这里的 virtual 关键字和继承里的是一样的,但是他们之间没什么关系,这里是为了实现多态,在继承中是为了解决菱形继承的问题。
2. 重写/覆盖
基类和派生类中有函数名、参数、返回值均相同的虚函数(还有两个例外)
虚函数的重写体现了继承关系间的多态
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
//子类的虚函数重写了父类的虚函数
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
class Soldier : public Person
{
public:
//子类的虚函数重写了父类的虚函数
virtual void BuyTicket()
{
cout << "优先-买票" << endl;
}
};
注意:派生类的 virtual 关键字可以不写,因为从基类继承下来时保留了虚函数的属性,也可以构成重写。但是不是很规范,建议还是写上。
当然也可以通过函数实现多态,注意要传基类的指针或引用。
void Func(Person& p)
{
p.BuyTicket();
}
void Func(Person* p)
{
p->BuyTicket();
}
int main()
{
Person p;
Student st;
Soldier sd;
Func(p); //买票-全价
Func(st); //买票-半价
Func(sd); //优先买票
Func(&p); //买票-全价
Func(&st); //买票-半价
Func(&sd); //优先买票
return 0;
}
【三个例外】
(1)协变:派生类和基类的虚函数的返回值不同,但是派生类虚函数的返回值必须是基类返回值的子类型
【示例代码】
class A
{};
class B : public A
{};
class Person
{
public:
//返回基类A的指针
virtual A* fun()
{
cout << "A* Person::f()" << endl;
return new A;
}
};
class Student : public Person
{
public:
//返回子类B的指针
virtual B* fun()
{
cout << "B* Student::f()" << endl;
return new B;
}
};
int main()
{
Person p;
Person* ptr1 = &p;
Person* ptr2 = &st;
//父类指针ptr1指向的p是父类对象,调用父类的虚函数
ptr1->fun(); //A* Person::f()
//父类指针ptr2指向的st是子类对象,调用子类的虚函数
ptr2->fun(); //B* Student::f()
return 0;
}
(2)析构函数的重写:基类和派生类的析构函数名不同
这么做是为了防止一下这种情况:
int main()
{
//分别new一个父类对象和子类对象,并均用父类指针指向它们
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
如果两个 delete 都是调用的父类的析构函数,那么子类对象开辟的空间就无法释放,导致内存泄露。
【callback】在继承那篇文章里我们说过,任何类的析构函数名都会被编译器处理成 destructor,所以基类和派生类的析构函数会构成重定义/隐藏,如果需要调用此继承关系中的析构函数,需要用域作用限定符。
(3)派生类的虚函数可以不加virtual
补充:三个概念的比较
3. override和final
有的时候我们很容易就会因为函数名、参数写错,导致子类没有重写父类的虚函数。而且在编译期间是不会报错的,如果等到运行时再来调试就会很麻烦了。而关键字 override 就会检测我们是否正确地重写了。
关键字 final 则用于修饰父类的虚函数,修饰后子类不能重写这个虚函数,否则会编译报错
【示例代码】
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
//子类完成了父类虚函数的重写,编译通过
virtual void BuyTicket() override
{
cout << "买票-半价" << endl;
}
};
class Person
{
public:
//被final修饰,该虚函数不能再被重写
virtual void BuyTicket() final
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
//编译报错
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
4. 纯虚函数和抽象类
在虚函数的后面加上 = 0 即为纯虚函数,而包含纯虚函数的类就叫做抽象类,抽象类不能实例化出对象。抽象类的派生类只有在重写了虚函数之后,才能实例化出对象。
【示例代码】
class Car
{
public:
//纯虚函数
virtual void Drive() = 0;
};
class Benz : public Car
{
public:
//重写纯虚函数
virtual void Drive()
{
cout << "Benz-drive" << endl;
}
};
class BMV : public Car
{
public:
//重写纯虚函数
virtual void Drive()
{
cout << "BMV-drive" << endl;
}
};
int main()
{
Benz b1;
BMV b2;
//不同对象用基类指针调用Drive函数,完成不同的行为
Car* p1 = &b1;
Car* p2 = &b2;
p1->Drive(); //Benz-drive
p2->Drive(); //BMV-drive
return 0;
}
Q:抽象类的意义是?
顾名思义,不能用它来代指一个具体的对象,只能代表一类事物。而且也能强制子类继承接口,若不重写,也无法实例化出对象。
补充:普通函数的继承是实现继承,也就是说派生类可以直接使用基类的函数。而虚函数的继承是一种接口继承,为的就是实现多态,让虚函数实现重写。
多态的原理
1. 虚表和虚表指针
我们先来看这段代码的运行结果:
结果分析:在b对象中,不仅仅包含了一个 _b 成员变量,还包括了一个虚表指针,该指针指向虚表(虚函数表)。每一个含有虚函数的类所形成的对象都有虚表指针。
我们再通过一个更加复杂的例子来看虚表里面到底放什么。
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 = 1;
};
class Derive : public Base
{
public:
//重写
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
这里我做了一个图,方便大家理解
虚表中保存的是虚函数的地址,而Func3只是普通的成员函数,所以不保存在其中。并且我们可以看到子类对象的重写发挥了作用,子类的Func2函数地址覆盖了父类的地址,这也就是重写也叫覆盖的原因。在语法层我们说重写,而在原理层我们就称为覆盖。
虚函数表本质是一个存虚函数指针的指针数组,一般情况下会在这个数组最后放一个nullptr。
【一般步骤】
1. 将基类的虚表的内容拷贝到派生类的虚表中
2. 如果派生类重写了基类的虚函数,那就将其覆盖
3. 派生类新增的虚函数按声明的顺序存放到虚表中
【注意点】
1. 虚表是在构造函数初始化列表阶段进行初始化的。
2. 虚表中存的是虚函数的地址,而不是虚函数,虚函数也是函数,是存在代码段中的。
3. 对象中存的不是虚表,而是指向虚表的指针。
2. 原理?
所以有了上面只是的铺垫,下面就可以来聊聊多态的原理了,还是以之前的代码为例:
class Car
{
public:
virtual void Drive() = 0;
};
class Benz : public Car
{
public:
virtual void Drive()
{
cout << "Benz-drive" << endl;
}
};
class BMV : public Car
{
public:
virtual void Drive()
{
cout << "BMV-drive" << endl;
}
};
int main()
{
Benz b1;
BMV b2;
Car* p1 = &b1;
Car* p2 = &b2;
p1->Drive(); //Benz-drive
p2->Drive(); //BMV-drive
return 0;
}
当父类的指针指向不同类的子类对象时,会分别到各自的虚表中找到对应的虚函数地址,执行不同的函数,实现多态。所以构成多态的条件,第一对父类的虚函数完成重写,如果不重写就会调用父类的虚函数,无法实现多态。
那为什么是使用父类的指针而不是父类的对象呢?这是因为父类的对象会对子类的对象进行拷贝构造,最后虚表指针的指向都是父类自己的虚表,调用父类的虚函数,无法构成多态。
动态绑定和静态绑定
1. 动态绑定又称动态多态、后期绑定、晚绑定,指的是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体函数,比如利用虚函数来实现的多态。
2. 静态绑定又称静态多态、前期绑定、早绑定,在程序编译期间确定了程序的行为,比如函数重载。
虚基表和虚基表指针
在使用菱形虚拟继承时(不推荐使用),如果此时还用虚函数实现了多态,那么就引入了虚基表指针,用来指向自己不同的父类的虚表指针相对于此指针的偏移量。
注意区分虚表和虚基表不是同一概念!!
感谢你能看到这里,如果觉得有帮助的话不妨点个赞!