目录
第一节:多态尝试
既然多态是不同类调用同一个函数达成不同结果,那么子类直接完成函数的重定义不就行了吗?
// 基类
class Person {
public:
void BuyTicket() { // 一般人全价
std::cout << "全价" << std::endl;
}
};
// 子类
class Student:public Person {
public:
void BuyTicket() { // 学生半价
std::cout << "半价" << std::endl;
}
};
int main() {
Person p1;
Student st1;
p1.BuyTicket();
st1.BuyTicket();
return 0;
}
这样似乎可以达到目的,但是需要注意的是父类Person的BuyTicket只是被隐藏了,而不是被覆盖了,所以使用Person类型的指针接收st1后,用指针调用到的还是父类的BuyTicket:
int main() {
Person p1;
Student st1;
Person* ptr1 = &st1; // 父类指针接收
p1.BuyTicket();
ptr1->BuyTicket(); // 父类指针调用
return 0;
}
ptr1虽然类型是父类,但是它指向的是子类,那么按照常理来说应该是半价才对,所以只使用函数重写是不能达到多态的要求的。
实现多态还需要 virtual 关键字。
第二节:virtual
virtual可以用来修饰成员函数,将成员函数变成虚函数,虚函数与普通成员函数的区别是:
虚函数不保存在类中,而是在类中形成一个虚表指针,这个指针指向一个虚函数表(简称虚表),虚函数表用于存放虚函数的地址。
2-1.多态原理
子类继承了拥有虚表指针的父类之后会拷贝一份父类虚表到子类虚表中,然后新增的虚函数依次放在后面。当子类对父类虚函数进行重写时,子类虚表位置的父类虚函数不变,虚表会新增重写后的虚函数地址,同时将原本指向父类虚函数的条目重定向为重写后的虚函数。这样就可以保证指针指向哪个类,就调用哪个类的虚函数了,而与指针类型无关。
故上述代码修改为:
// 基类
class Person {
public:
virtual void BuyTicket() { // 设置成虚函数
std::cout << "全价" << std::endl;
}
};
// 子类
class Student :public Person {
public:
void BuyTicket() { // 重写虚函数
std::cout << "半价" << std::endl;
}
};
int main() {
Person p1;
Student st1;
Person* ptr1 = &st1; // 父类指针接收
p1.BuyTicket();
ptr1->BuyTicket(); // 父类指针调用
return 0;
}
上述代码的实现逻辑是这样的:
综上可得多态的第一个条件是:virtual修饰获得虚函数,该虚函数的重写。
2-2.虚函数重写
虚函数重写与函数重定义还是有区别的,虚函数重写需要返回值、函数名、参数类型相同。当然也有例外,例如返回值 父类* 、子类* 被认为是相同的, 父类&、子类& 被认为是相同的。
其次虚表中被重写的父类虚函数地址没有被删除,那么子类可以通过显式调用的方式访问父类虚函数:
int main() {
Person p1;
Student st1;
st1.Person::BuyTicket(); // 显式调用
return 0;
}
2-3.析构函数重写
析构函数也能进行重写,主要应用场景如下:
// 基类
class Person {
public:
virtual ~Person() {
std::cout << "父类析构" << std::endl;
}
};
// 子类
class Student :public Person {
public:
~Student() {
std::cout << "子类析构" << std::endl;
}
};
int main() {
Person* st1 = new Student;
delete st1;
return 0;
}
上述代码的结果是什么呢?我们来分析一下:
st1虽然是Person*类型的,但是它指向Student,现在有一个问题:~Person和~Student看起来名字不一样,它们构成重写吗?前面我们说过析构函数实际上的名字都是destructor,所以它们是构成重写的。
而delete会调用析构函数,那么它的类型虽然是 Person* 但是它会调用到重写后的~Student函数,又因为子类析构会调用父类析构,所以结果一个是先打印一次"子类析构",再打印一次"父类析构":
如果将~Person前的 virtual 去掉,那么 delete 就不会访问虚表,而是直接调用父类的析构:
完成析构函数的重写后就可以实现指向谁就调用谁的析构函数,而与指针类型无关。
第三节:多态
之前已经说到多态的原理是子类继承的父类切片中,虚表指针被替换成了子类的虚表指针,所指向的虚表是新拷贝的虚表,所以只需要子类当中的父类切片就能实现访问子类的虚表了,这就意味着多态的实现只需要子类中的父类切片。
那么就可以定义如下的一个函数:
void Func(Person& p) {
p.BuyTicket();
}
虽然参数类型是Person,但是由于隐式类型转化的存在,也可以传入Student:
void Func(Person& p) {
p.BuyTicket();
}
int main() {
Student st1;
Func(st1);
return 0;
}
此时的 p 获得的就是 st1 的Person切片,故执行结果为:
而传入指向Person对象的指针,结果自然是"全价"。
这就是形成多态的另一大条件:参数为父类的引用或指针(获得子类的父类切片)
第四节:多继承的子类
如果一个子类继承两个父类,而这两个父类都有虚表指针,那么子类会拥有两个虚表指针,每个父类切片包含一个:
class A {
virtual void func_1() {}
};
class B {
virtual void func_2() {}
};
class C :public A, public B {
virtual void func_3() {} // 新增的虚函数
};
而子类新增的虚函数地址存放在第一个父类切片的虚表中,而不是每个父类切片都保存:
第五节:相关知识
这里补充一些类的其他相关知识。
5-1.final 关键字
final 意味着"最后的",它可以修饰虚函数和类。
5-1-1.修饰虚函数
被其修饰的虚函数不允许重写,否则会报错:
5-1-2.修饰类
被 final 修饰的类不允许被继承,否则会报错:
5-2.override 关键字
override 意味着"推翻、凌驾",它可以修饰虚函数。
它用于修饰子类重写后的新的虚函数后,作用是检查该虚函数是否成功重写:
(1)重写成功,不报错:
(2)名字改变,重写失败,报错:
5-3.纯虚函数与抽象类
5-3-1.纯虚函数
纯虚函数是只有声明,没有实现的虚函数:
class car {
public:
virtual void Drive() = 0; // 纯虚函数
};
5-3-2.抽象类
只要拥有纯虚函数的类就是抽象类,与其是否有其次成员无关。
抽象类的特点是无法被实例化,但是可以被继承:
class BYD:public Car{};
但是因为其子类也继承了该纯虚函数,所以子类也无法被实例化,但是如果子类将纯虚函数全部进行重写,就可以实例化了:
class BYD:public Car{
public:
void Drive(){} // 重写纯虚函数
};
所以抽象类的意义就是提供一个可继承、不需要实例化的基类。
第六节:下期预告
类的三大特性:封装、继承、多态,就告一段落了,之后将进入一些数据结构的学习,它们分别是:二叉搜索树、map和set、AVL树、红黑树、哈希表。