C++中的虚函数和多态
- 1.引入多态
- 2.实现多态---虚函数实现
-
- [2.1 语法规则:](#2.1 语法规则:)
- 3.多态的特点和要求
- 4.多态(虚函数)的底层原理
- 5.小结
- 6.多态的分类
- 练习:
- 7.重载和重写(覆盖),隐藏的区别
-
- [7.1 重写(覆盖,复写) -->是指子类函数覆盖基类函数,子类重新定义了父类的同名方法(虚函数)](#7.1 重写(覆盖,复写) -->是指子类函数覆盖基类函数,子类重新定义了父类的同名方法(虚函数))
- [7.2 重载 -->发生在同一个类的里面](#7.2 重载 -->发生在同一个类的里面)
- [7.3 隐藏 -->子类和父类的函数名相同](#7.3 隐藏 -->子类和父类的函数名相同)
- 8.父类的同名函数是虚函数和普通函数(非虚函数)的区别
-
- [8.1 虚函数](#8.1 虚函数)
- [8.2 普通函数(非虚函数)](#8.2 普通函数(非虚函数))
- C++的函数分成两大类
1.引入多态
多态:同一个方法在父类和子类中具备不同的表现形式,传递不同子类可以使用不同子类的同名方法--》叫做多态
字面上理解就是多种表现形式
,
比如: Animal Cat Dog Sheep 都有eat(),但是eat方法具备了多种表现形式
C++允许父类的指针或者引用指向不同的子类对象,传递实参的时候,传递不同子类对象,到时候可以调用不同子类里面的同名方法 ---》多态
示例代码:引入多态
cpp
#include <iostream>
#include <cstring>
using namespace std;
/*
引入多态:
面向对象三大思想:类封装,继承和派生,多态
封装一个函数,要求该函数可以展示各种动物吃什么?
*/
class Animal
{
public:
void eat()
{
cout<<"动物吃"<<endl;
}
};
class Cat:public Animal
{
public:
void eat()
{
cout<<"猫吃鱼"<<endl;
}
};
class Dog:public Animal
{
public:
void eat()
{
cout<<"狗吃骨头"<<endl;
}
};
class Sheep:public Animal
{
public:
void eat()
{
cout<<"羊吃草"<<endl;
}
};
// 封装一个函数,要求该函数可以展示各种动物吃什么?
/*
思考1:需要参数,参数要具有通用性(可以兼容所有的动物)
答案:C++规定,有了继承之后,父类的指针或者父类的引用可以直接指向不同的子类对象(不需要使用任何强转类型转换)
父类 *p=&子类
父类 &b=子类
思考2:我们希望传递不同的子类对象,可以调用不同子类里面的同名eat方法
答案:必须使用虚函数才能解决,此时无法解决
*/
//void showAnimalEat(Animal &other) //只要是Animal的子类都可以兼容
void showAnimalEat(Animal *other) //只要是Animal的子类都可以兼容
{
//调用各个动物里面的eat方法
other->eat();
}
int main(int argc,char **argv)
{
// 定义各种动物对象
Cat c1;
Dog d1;
Sheep s1;
Animal a1;
Animal &p = c1;
/* 当方法不是虚函数时,编译器根据 指针/引用的声明类型(而不是实际对象类型)
决定调用哪个方法 这里 p 是 Animal& 类型(基类引用),因此 p.eat() 始终调用
Animal::eat()*/
p.eat();
//showAnimalEat(c1);
//showAnimalEat(d1);
//showAnimalEat(s1);
showAnimalEat(&c1);
showAnimalEat(&d1);
showAnimalEat(&s1);
return 0;
}
/*
执行结果:
动物吃
动物吃
动物吃
动物吃
*/
2.实现多态---虚函数实现
虚函数是为了多态而生的
示例代码:虚函数实现
cpp
#include <iostream>
#include <cstring>
using namespace std;
/*
引入多态:
面向对象三大思想:类封装,继承和派生,多态
封装一个函数,要求该函数可以展示各种动物吃什么?
*/
class Animal
{
public:
// 在基类函数中添加virtual关键字
virtual void eat()
{
cout<<"动物吃"<<endl;
}
};
class Cat:public Animal
{
public:
void eat()
{
cout<<"猫吃鱼"<<endl;
}
};
class Dog:public Animal
{
public:
void eat()
{
cout<<"狗吃骨头"<<endl;
}
};
class Sheep:public Animal
{
public:
void eat()
{
cout<<"羊吃草"<<endl;
}
};
// 封装一个函数,要求该函数可以展示各种动物吃什么?
/*
思考1:需要参数,参数要具有通用性(可以兼容所有的动物)
答案:C++规定,有了继承之后,父类的指针或者父类的引用可以直接指向不同的子类对象(不需要使用任何强转类型转换)
父类 *p=&子类
父类 &b=子类
思考2:我们希望传递不同的子类对象,可以调用不同子类里面的同名eat方法
答案:必须使用虚函数才能解决,此时无法解决
*/
//void showAnimalEat(Animal &other) //只要是Animal的子类都可以兼容
void showAnimalEat(Animal *other) //只要是Animal的子类都可以兼容
{
//调用各个动物里面的eat方法
other->eat();
}
int main(int argc,char **argv)
{
// 定义各种动物对象
Cat c1;
Dog d1;
Sheep s1;
Animal a1;
Animal &p = c1;
// 定义基类为虚函数后调用的是子类的方法
p.eat();
//showAnimalEat(c1);
//showAnimalEat(d1);
//showAnimalEat(s1);
showAnimalEat(&c1);
showAnimalEat(&d1);
showAnimalEat(&s1);
return 0;
}
/*
执行结果:
猫吃鱼
猫吃鱼
狗吃骨头
羊吃草
*/
2.1 语法规则:
cpp
virtual 返回值 函数名(参数)
{
}
3.多态的特点和要求
- 必须要有继承,没有继承,就没有多态(父类的指针/引用可以指向不同的子类对象)
- 子类必须要重写父类的同名方法
- 父类的同名方法必须定义成虚函数
注意:父类的同名方法定义成了虚函数,所有子类中同名的方法全部都默认是虚函数(子类加不加virtual都行)
4.多态(虚函数)的底层原理
- 虚函数表(虚表):C++中专门用来存放虚函数地址的一种数据结构(本质是个存放函数地址的数组)
- 一个类只要定义了虚函数,该类所有的对象中会新增一个指针,该指针用来指向虚函数表(虚表)的首地址
- 一个类中定义了虚函数,那么这个类以及它派生出来的子类都会有各自独立的虚函数表
- 父类的指针或者父类的引用去调用方法的时候,其实就是去查询虚函数表
示例代码:一个类只要定义了虚函数,该类所有的对象中会新增一个指针,该指针用来指向虚函数表(虚表)的首地址
cpp
#include <iostream>
#include <cstring>
using namespace std;
/*
一个类只要定义了虚函数,该类所有的对象中会新增一个指针,
该指针用来指向虚函数表(虚表)的首地址
*/
class Animal
{
public:
virtual void eat()
{
cout<<"动物吃"<<endl;
}
};
class Cat:public Animal
{
public:
void eat()
{
cout<<"猫吃鱼"<<endl;
}
};
class Dog:public Animal
{
public:
void eat()
{
cout<<"狗吃骨头"<<endl;
}
};
class Sheep:public Animal
{
public:
void eat()
{
cout<<"羊吃草"<<endl;
}
};
int main(int argc,char **argv)
{
cout<<"sizeof(Animal):"<<sizeof(Animal)<<endl;
cout<<"sizeof(Cat):"<<sizeof(Cat)<<endl;
cout<<"sizeof(Dog):"<<sizeof(Dog)<<endl;
cout<<"sizeof(Sheep):"<<sizeof(Sheep)<<endl;
return 0;
}
/*
执行结果:
sizeof(Animal):8
sizeof(Cat):8
sizeof(Dog):8
sizeof(Sheep):8
*/
示例代码:证明子类重写了父类的虚函数,父类的虚函数会被替换
cpp
#include <iostream>
#include <cstring>
using namespace std;
/*
引入多态:
面向对象三大思想:类封装,继承和派生,多态
封装一个函数,要求该函数可以展示各种动物吃什么?
*/
class Animal
{
public:
virtual void eat()
{
cout<<"动物吃"<<endl;
}
};
class Cat:public Animal
{
public:
// 如果子类重写了父类的虚函数,父类的虚函数会被替换
void eat()
{
cout<<"===子类调用父类的虚函数方法====="<<endl;
Animal::eat(); //调用父类的虚函数方法
cout<<"===子类调用父类的虚函数方法====="<<endl;
cout<<"子类-->猫吃鱼"<<endl;
}
};
class Dog:public Animal
{
public:
// 如果子类不重写父类的虚函数,父类的虚函数会被保留
// void eat()
// {
// cout<<"狗吃骨头"<<endl;
// }
};
class Sheep:public Animal
{
public:
// 如果子类不重写父类的虚函数,父类的虚函数会被保留
// void eat()
// {
// cout<<"羊吃草"<<endl;
// }
};
// 封装一个函数,要求该函数可以展示各种动物吃什么?
/*
思考1:需要参数,参数要具有通用性(可以兼容所有的动物)
答案:C++规定,有了继承之后,父类的指针或者父类的引用可以直接指向不同的子类对象(不需要使用任何强转类型转换)
父类 *p=&子类
父类 &b=子类
思考2:我们希望传递不同的子类对象,可以调用不同子类里面的同名eat方法
答案:必须使用虚函数才能解决,此时无法解决
*/
//void showAnimalEat(Animal &other) //只要是Animal的子类都可以兼容
void showAnimalEat(Animal *other) //只要是Animal的子类都可以兼容
{
//调用各个动物里面的eat方法
other->eat();
}
int main(int argc,char **argv)
{
// 定义各种动物对象
Cat c1;
Dog d1;
Sheep s1;
Animal a1;
Animal &p = c1;
p.eat(); // ----》Animal &p = c1;调用的是子类的eat方法
//showAnimalEat(c1);
//showAnimalEat(d1);
//showAnimalEat(s1);
showAnimalEat(&c1); // 调用的是子类的eat方法
showAnimalEat(&d1); // ----》调用的是父类的eat方法
showAnimalEat(&s1); // 调用的是子类的eat方法
return 0;
}
/*
执行结果:
===子类调用父类的虚函数方法=====
动物吃
===子类调用父类的虚函数方法=====
子类-->猫吃鱼
===子类调用父类的虚函数方法=====
动物吃
===子类调用父类的虚函数方法=====
子类-->猫吃鱼
动物吃
动物吃
*/
5.小结
C++三大"虚"
- 第一:虚继承和虚基类
- 第二:虚函数和多态
- 第三:纯虚函数和抽象类
6.多态的分类
分为两大类:
- 编译时多态:指的就是函数重载
- 运行时多态(动态联编):指的就是目前学习的这种,父类的指针,引用指向不同的子类对象
练习:
示例代码:基类不是虚函数的情况
cpp
#include <iostream>
#include <cstring>
using namespace std;
/*
规律:
如果vf不是虚函数
此时四种情况究竟调用谁的vf,看赋值运算左边的指针类型即可
如果vf是虚函数
此时四种情况究竟调用谁的vf,看赋值运算右边的指针类型即可
*/
class B
{
public:
void vf()
{
cout<<"父类B里面的vf函数"<<endl;
}
};
class D:public B
{
public:
void vf()
{
cout<<"子类D里面的vf函数"<<endl;
}
};
int main(int argc,char **argv)
{
//定义父类和子类的对象,指针
B b;
B *pb;
D d;
D *pd;
//第一组
pb=&b; //父类指针指向父类对象
pb->vf();
pb=&d; //父类指针指向子类对象
pb->vf();
//第二组
pd=&d; //子类指针指向子类对象
pd->vf();
pd=(D *)&b; //子类指针指向父类对象
pd->vf();
return 0;
}
/*
执行结果:
父类B里面的vf函数
父类B里面的vf函数
子类D里面的vf函数
子类D里面的vf函数
*/
示例代码:基类是虚函数的情况
cpp
#include <iostream>
#include <cstring>
using namespace std;
/*
规律:
如果vf不是虚函数
此时四种情况究竟调用谁的vf,看赋值运算左边的指针类型即可
如果vf是虚函数
此时四种情况究竟调用谁的vf,看赋值运算右边的指针类型即可
*/
class B
{
public:
virtual void vf()
{
cout<<"父类B里面的vf函数"<<endl;
}
};
class D:public B
{
public:
void vf()
{
cout<<"子类D里面的vf函数"<<endl;
}
};
int main(int argc,char **argv)
{
//定义父类和子类的对象,指针
B b;
B *pb;
D d;
D *pd;
//第一组
pb=&b; //父类指针指向父类对象
pb->vf();
pb=&d; //父类指针指向子类对象
pb->vf();
//第二组
pd=&d; //子类指针指向子类对象
pd->vf();
pd=(D *)&b; //子类指针指向父类对象
pd->vf();
return 0;
}
/*
执行结果:
父类B里面的vf函数
子类D里面的vf函数
子类D里面的vf函数
父类B里面的vf函数
*/

7.重载和重写(覆盖),隐藏的区别
7.1 重写(覆盖,复写) -->是指子类函数覆盖基类函数,子类重新定义了父类的同名方法(虚函数)
要求:
- 在不同的类中(分别位于子类和父类)
- 同名同参,同返回值(此时返回值类型不可以不一样,返回值类型不一样编译报错)
- 基类的函数名前必须有virtual关键字
7.2 重载 -->发生在同一个类的里面
7.3 隐藏 -->子类和父类的函数名相同
- 如果派生类函数与基类函数同名,但参数不同,无论基类函数前是否有virtual修饰,基类函数被隐藏.
- 如果派生类函数与基类函数同名,参数也相同(不关心返回值类型),但是基类函数前无virtual修饰,基类函数被隐藏。
隐藏的具体表现是:
cpp
假设父类是void eat();
子类是void eat(int n);
Cat c;
c.eat(); //编译语法错误,原因是父类同名方法被隐藏了
c.Animal::eat(); //编译正确,父类被隐藏的同名方法只能通过类作用域解析运算符调用
c.eat(666);//编译正确
重写(覆盖,复写)的例子
cpp
class Animal
{
public:
virtual void eat();
};
class Dog:public Animal
{
public:
void eat(); //Dog重写(覆盖,复写)了父类Animal的eat方法
};
重载的例子
cpp
class Dog
{
public:
Dog();
Dog(int age); //同一个类中重载了构造了函数
void setAge();
void setAge(int age);
}
隐藏的例子1
cpp
class Animal
{
public:
virtual void eat();
};
class Dog:public Animal
{
public:
void eat(string); //父类的eat被隐藏
};
隐藏的例子2
cpp
class Animal
{
public:
void eat();
};
class Dog:public Animal
{
public:
void eat(string); //父类的eat被隐藏
};
隐藏的例子3
cpp
class Animal
{
public:
void eat();
};
class Dog:public Animal
{
public:
void eat(); //父类的eat被隐藏
};
8.父类的同名函数是虚函数和普通函数(非虚函数)的区别
8.1 虚函数
虚函数:编译器采用动态联编
动态联编:编译器会严格按照赋值运算右边的类型来调用对应的同名函数
8.2 普通函数(非虚函数)
普通函数(非虚函数):编译器采用静态联编
静态联编:编译器会严格按照赋值运算左边的类型来调用对应的同名函数
- 父类eat()函数不是虚函数:
cpp
Animal a;
Cat c;
Animal &r=c; //父类的引用指向子类对象
r.eat(); // 当父类eat()函数不是虚函数时此时调用父类的eat()
无论是指针还是引用,此时都以左边类型(Animal)为准
- 父类eat()函数是虚函数:
cpp
Animal a;
Cat c;
Animal &r=c;
r.eat(); // 父类eat()函数是虚函数,此时调用子类的eat()
无论是指针还是引用,此时都以右边类型(Cat )为准
C++的函数分成两大类
第一类:类的成员函数 ---》通过对象或者类名调用
第二类:非成员函数,普通函数 ---》直接调用