目录
一.多态的概念
- 多态分为编译时多态(静态多态),和运行时多态(动态多态),两者的区别在静态绑定和动态绑定章节讲,这里主要讲运行时多态
- 多态就是继承关系下的类对象,调用同一函数,产生不同的行为
二.多态的定义及实现
(1).虚函数
类成员函数前加virtual修饰,
class Person
{
public:
virtual void BuyTicket()
{cout << "买票全价" << endl;}
};
(2).虚函数的重写(覆盖)
派生类中有一个和基类完全相同的虚函数(函数名,返回值,参数列表),
注意:在重写虚函数时,派生类也可以不加virtual关键字,同样构成多态,因为virtual在继承时已经被继承下来了
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票半价" << endl;
}
};
(3).实现多态的条件
- 必须是基类的指针或者引用调用虚函数,因为只有基类才能既能指向基类又能指向派生类
- 被调用的函数必须是虚函数,并且完成了虚函数重写(覆盖)。完成重写才能使基类和派生类才能有不同的函数,才能实现多态的效果
(4).多态场景题
class A
{
public:
virtual void func(int val = 1)
{
std::cout << "A->" << val << std::endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
void func(int val = 0)
{
std::cout << "B->" << val << std::endl;
}
};
int main()
{
B* p = new B;
p->test();
return 0;
}
程序运行结果是 B->1
解释:函数重写本质上只重写了函数体部分,参数及缺省值没有改变;
(5).析构函数的重写
- 基类的析构函数为虚函数时,无论派生类的析构函数加不加virtual,都构成析构函数的重写,因为在编译器会对析构函数做特殊处理,所有的析构函数都变成destructor()
- 基类的析构函数一般会设计成虚函数防止有些情况会出现内存泄漏,比如下面这种情况,当派生类成员赋给基类时,而且析构函数没有构成重写,就会只调用基类的析构函数,而派生类中的_p没有释放,造成内存泄漏,这里可以和析构函数的调用顺序对比理解一下---------->>>派生类的默认成员函数函数
class Person
{
public:
~Person()
{
cout << "析构基类" << endl;
}
};
class Student : public Person
{
public:
~Student()
{
cout << "析构派生类" << endl;
delete _p;
}
int* _p = new int[10];
};
int main()
{
Person* per=new Person ;
Person *stu=new Student;
//delete per;
delete stu;
return 0;
}
(6).override和final关键字
- 在重写虚函数时,有时会由于疏忽犯写错函数名之类的错误,这些错误在编译期间是不会报错的,override可以检测是否重写
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket_NO() override //函数名写错
{
cout << "买票半价" << endl;
}
};
- 不想让派生类重写这个虚函数,时可以让final修饰
class Person
{
public:
virtual void BuyTicket() final
{
cout << "买票全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票半价" << endl;
}
};
三.纯虚函数和抽象类
- 在虚函数后面写上 =0,这个函数称为纯虚函数(这个函数不需要实现,没有意义),这个类称为抽象类,不能实例化出对象,如果派生类继承了之后不重写这个纯虚函数,派生类也是抽象类,不能实例化出对象(强制重写)。
class Person
{
public:
virtual void BuyTicket() =0 //纯虚函数,抽象类
{
cout << "买票全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票半价" << endl;
}
};
int main()
{
Person* per = new Person; //错误,不能用抽象类实例化对象
Person* stu = new Student;
delete stu;
return 0;
}
四.多态的原理
(1.)虚函数表指针的存在
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票全价" << endl;
}
int _id;
int _adress;
};
int main(){
Person per;
sizeof(per);
return 0;
}
从这里可以看出,Person中不仅有_id和_adders成员变量,还有一个指针变量,这个时虚函数表指针,用来存放虚函数的地址,一个类所有虚函数的地址都会被放到这个个表里,一个有虚函数的类一定会有这个指针变量
(2.)虚函数表
- 虚函数表本质上是一个指针数组,用来存放类中所有虚函数的地址,这个数组的指针存在类中
- 基类对象的虚函数表存放基类所有的虚函数地址,不同类型的对象有各自不同的虚表,所以基类和派生类有各自不同的虚表
- 派生类由继承下来的基类和自己的成员组成,如果基类中有虚表,就不会在生成新的虚表。但是这里的虚表是独立的,像【基类对象】和【派生类对象中的基类成员】一样
- 派生类中重写的基类虚函数,虚表中的地址就会被覆盖成重写的函数地址
- 派生的虚表由 <1>基类的虚函数地址 <2> 重写的虚函数覆盖原来的地址 <3> 派生类自己的虚函数地址 这三部分构成
- 虚函数和普通函数一样,都放在代码段(提醒自己::看到这要去学内存分布)
class Person
{
public:
virtual void func_Person() { cout << "Person :: func_Person()"; }
virtual void func0() { cout << "Person :: func0()"; }
virtual void func1() { cout << "Person :: func1()"; }
virtual void func2() { cout << "Person :: func2()"; }
void func_normal() { cout << "Person :: func_normal()"; }
public:
int _id;
int _adress;
};
class Student : public Person
{
public:
virtual void func_Student() { cout << "Student :: func_Student()"; }
virtual void func1() { cout << "Student :: func1()"; }
virtual void func2() { cout << "Student :: func2()"; }
void func_normal() { cout << "Student :: func_normal()"; }
public:
int _num;
};
这里没有列出派生类自己的虚函数是由于编译器的优化,并不是不存在;
(3.) 多态的实现
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票半价" << endl;
}
};
(4.)动态绑定与静态绑定
- 对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。(编译阶段是指将编译语言转换为机器语言的阶段)
- 满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。 (运行阶段指程序的可执行文件被加载到计算机内存中,并由操作系统调度执行的过程。这个阶段是程序实际发挥作用的阶段。)