C++(进阶) 第2章 多态
C++(进阶) 第2章 多态
文章目录
- 前言
- 一、多态的概念
- 二、多态的定义及实现
- 1.虚函数
- 2.虚函数的重写
- 3.多态的条件
- 4.多态的细节
- 三、析构函数的重写
- 四、重载/重写/隐藏的对比
- 五、抽象类
- 抽象类
- 六、相关题目
- 题目1
- 题目2
- 七、const修饰
- 八、多态原理
- 九、虚函数放在地方
- 总结
前言
什么是多态?多态其实就是多种形态的简写这篇博客会详细的介绍多态
一、多态的概念
多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运⾏时多态(动态多态),这⾥我们重点讲运⾏时多态,编译时多态(静态多态)和运⾏时多态(动态多态)。编译时多态(静态多态)主要就是我们前⾯讲的函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态。
通俗点来说就是不同的东西做不同的事情有多种形态
二、多态的定义及实现
1.虚函数
类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修饰。
这是构成多态的必备条件之一
class Person{public:virtual void BuyTicket() { cout << "买票-全价" << endl; }};
2.虚函数的重写
这里学生类继承了person类那么这里按之前的理解应该构成隐藏关系但是这里不一样,这里我们一般叫做重写
class Person{public:virtual void BuyTicket() { cout << "买票-全价" << endl; }};
class Student : public Person
{
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
假如我现在要去买票
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;Student s;func(p);func(s);return 0;
}
注意看这里传入的是父类
这里可能会有疑问为什么func函数参数写的父类但是这里传入子类也可以这是因为切片
这里可以看到传入不同的类型处理的方式也不一样这里就是一个简单的多态
3.多态的条件
- 虚函数的重写
- 必须是父类的指针或者是引用
- 上面俩个条件缺一不可
4.多态的细节
假如这里把父类的virtual去掉这里是多态吗?
class Person{public:void BuyTicket() { cout << "买票-全价" << endl; }};
class Student : public Person
{
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
};void func(Person& p)
{p.BuyTicket();
}
int main()
{Person p;Student s;func(p);func(s);return 0;
}
这里的答案是:不是多态
假如这里子类去掉virtual是多态吗?
答案是:是多态
面试题就很喜欢这样考,但是这里是为什么父类去就不行子类就行呢?
上面那样构成虚函数的重写就算不写virtual,因为继承了父类子类哪里会把父类的声明拿下来也是属于多态
多态调用:看指向对象类型,指向谁调用谁的虚函数
普通调用:看调用者的类型,调用调用者的函数
三、析构函数的重写
假如这里有1父1子
class person
{
public:~person(){cout << "~person()" << endl;}};class student:public person
{
public:~student(){cout << "~student()" << endl;}
};int main()
{person* p1 = new student;delete p1;
}
这里定义了一个指针指向了student这里
但是这里调用的是父类的析构函数
上面这里会出现非常大的问题上面的代码是为了方便理解
假如我把代码改成这样
class person
{
public:~person(){cout << "~person()" << endl;}};class student:public person
{
public:~student(){delete[] _ptr;cout << "~student()" << endl;}
protected:int* _ptr = new int[10];
};int main()
{person* p1 = new student;delete p1;
}
可以看到这里就出现了很严重的内存泄露
按正常关系来说studen继承了person,这studen就应该有俩个析构函数一个自己的一个父类的但是这里为什么只调用了一个
这里是因为,析构函数会被编译器统一处理成destructor()那么这里就有俩个destructor函数这里就会成隐藏关系
所以这里就只会调用父类的析构函数
这里的解决方式就是加上virtual
四、重载/重写/隐藏的对比
五、抽象类
抽象类
class Car
{
public:virtual void Drive() = 0;
};
这就是一个抽象类,在虚函数的后⾯写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写,但是语法上可以实现),只要声明即可。
抽象类也无法实例化,这里就会有一个疑问了,那有个蛋用呢?
如果我不想让别人实例化出car类出来但是可以实例化benz出来就可以用抽象派去重写,继承的benz类是可以实例化的
class Car
{
public:virtual void Drive() = 0;
};class Benz : public Car
{
public:virtual void Drive(){cout << "Benz-舒适" << endl; }
};
int main()
{Benz a;
}
先来俩题热热身
六、相关题目
题目1
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;}
答案是B
这里就是c++语法的一个大坑,用的是B类,但是这里是继承的test会去找子类的,但是可以发现子类哪里是可以不写virtual的,用的是子类的定义,但是声明依旧用的是父类的
题目2
class Base
{
public:virtual void Fun1(){cout << "Func1" << endl;}
private:int _a = 1;char ch = '1';};int main()
{cout << sizeof(Base) << endl;
}
定义成虚函数了以后这里就多要一个指针
所以说写成虚函数就会付出代价,这个指针指向虚函数表,虚函数的地址都会放在这张表里面,多态就是靠这张虚函数表实现的
七、const修饰
纯虚函数无法实例化,这里可以强制别人用引用或者指针
class Animal
{
public:virtual void sound() = 0;
};class Cat : public Animal
{
public:virtual void sound(){cout << "Cat-喵喵" << endl;}
};
class Dog : public Animal
{
public:virtual void sound(){cout << "Dog-汪汪" << endl;}
};
void AnimalSound(Animal a)
{}
这里加上const可以使用匿名对象
class Animal
{
public:virtual void sound() const = 0;
};class Cat : public Animal
{
public:virtual void sound()const {cout << "Cat-喵喵" << endl;}
};
class Dog : public Animal
{
public:virtual void sound()const {cout << "Dog-汪汪" << endl;}
};
void AnimalSound(const Animal& a)
{a.sound();
}
int main()
{AnimalSound(Cat());return 0;
}
八、多态原理
上面说到多态必须是要虚函数并且形参还需要是父类的引用或者指针,那么这个虚函数和普通的函数有什么区别呢?
这里可以看到这里除了成员变量_a外还有一个_vfptr,这里面还放着成员函数的,这个东西其实就是一个指针数组,这个指针会指向一个表这个表就是虚表
下面俩个是虚函数一个不是,这里用监视窗口查看一下
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 _a;};int main()
{Base b;return 0;
}
这里可以通过上面看到这个表里面只有俩个函数
用内存窗口是可以看见的
多态其实就是动态确定地址
举个例子
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 _a;};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1" << endl;}
private:int _b;
};
void Func1(Base* p)
{p->Func1();//虚函数p->Func3();//不是虚函数
}int main()
{Func1(new Base);Func1(new Derive);return 0;
}
这里可以看见func1是重写后的那个func1但是这里的func3是不会重写的
第二个func1是重写后的那个func也就是谁调用那个虚表就指向,
第二个调用的时候我这里给的参数是Derive,这里会发生切片,这里就会对func1进行重写,但是这里收到的参数始终都是一个base指针,只不过这里可能是子类切片出来的,切片和不是切片出来的这里就会出现虚表地址不同
所以这里就叫运行时绑定也叫多态绑定,这里就可以指向父类调用父类,指向子类就调用子类,它能调用不同的函数,它本质的原因就是因为那个虚表的地址不同
这也就是为什么函数的参数必需要父类的指针或者引用的原因
九、虚函数放在地方
- 虚函数 ------------代码段
- 虚函数表 --------代码段
- 虚函数表的指针-类对象
总结
多态看起来复杂其实只要理解了虚表这个东西就不会复杂,虚函数就有一个指针数组,这里面放着虚表的地址,然后这些虚表里面放着虚函数的地址,然后在通过切片来看是哪一个虚表的地址这就是多态的原理