目录
1.函数重写(隐藏)
定义:子类重新定义父类中有相同名称、返回值类型和参数的虚函数,主要在继承关系中出现。
基本条件:
1.重写的函数和被重写的函数必须为虚函数(virtual),并且分别位于基类和派生类中。
2.重写的函数和被重写的函数,返回值、函数名和函数参数必须完全一致。
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak()
{
cout << "动物会说话" << endl;
}
};
class Cat : public Animal {
public:
virtual void speak() { // 函数重写,这个virtual可写可不写
cout << "小猫会喵喵叫" << endl;
}
};
int main() {
Cat cat;
cat.speak();
return 0;
}
2.函数隐藏
当父类和子类中有同名函数,子类中的函数会将父类中的同名函数隐藏
注意:在子类中和父类函数名字相同不是重写就是函数隐藏,注意这里指的是函数的名字,不包括函数的参数和返回值。
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak()
{
cout << "动物会说话" << endl;
}
void work()
{
cout << "动物会工作" << endl;
}
};
class Cat : public Animal {
public:
virtual void speak()// 函数重写,这个virtual可写可不写
{
cout << "小猫会喵喵叫" << endl;
}
void work() // 函数隐藏,因为不是虚函数
{
cout << "小猫会工作" << endl;
}
void work(int a) // 函数隐藏(参数不同)
{
cout << "小猫会工作" << endl;
}
//int work() //函数隐藏(返回值类型不同)
//{
// cout << "小猫会算数" << endl;
// return 6;
//}
};
int main() {
Cat cat;
cat.work();
return 0;
}
3.多态的基本概念
定义:通过基类指针或引用调用派生类的函数实现不同的行为
分类:
静态多态:函数重载和运算符重载属于静态多态,复用函数名
动态多态:派生类和虚函数实现运行时多态
区别:
1.静态多态的函数地址绑定是早绑定——编译节点确定函数地址
2.动态多态的函数地址是晚绑定——运行时阶段确定函数地址
原因:静态多态基于编译时就能确定的信息(如函数重载的参数特征、模板的参数类型等)来绑定函数地址,而动态多态由于要依据对象的实际类型,而这个类型在编译时往往不确定,需要在运行时通过虚函数表等机制去动态查找并绑定函数地址,所以分别呈现出早绑定和晚绑定的特性。
多态满足的条件
1.有继承关系
2.子类重写父类中的虚函数
多态使用的条件
父类指针或引用指向子类对象
#include <iostream>
using namespace std;
class animal {
public:
virtual void speak() { cout << "动物会说话" << endl; }
void work() { cout << "动物会上班" << endl; }
};
class cat : public animal {
public:
virtual void speak() { cout << "小猫会喵喵叫" << endl; }
void work() { cout << "猫会上班" << endl; }
};
class dog : public animal {
public:
virtual void speak() { cout << "小狗会旺旺叫" << endl; }
void work() { cout << "小狗会上班" << endl; }
};
void Speak(animal& animal) { // 通过引用指向子类对象
animal.speak();
}
int main() {
// 动态多态:父类的指针或者引用指向子类对象,并且通过该指针或者引用调用子类重写的虚函数
animal* a = new dog;
a->speak();
a->work(); // 调用的函数是隐藏函数时,调用函数的指针或对象是什么类型调用哪里的函数
return 0;
}
例如在上述代码中,两个子类Cat和Dog都继承父类animal,然后在子类中都对speak虚函数进行了重写,而work函数则是实现的是函数隐藏。在主函数中定义了一个父类函数的指针a,然后它指向的是子类对象dog,然后调用的是子类重写的虚函数speak,所以就会执行子类的speak函数。而work函数由于是函数隐藏,所以调用函数的指针是什么类型就调用哪儿的函数,运行结果如下。
4.多态的实现
为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心就是虚函数表。
类的虚函数表:
1.每个包含了虚函数的类都包含一个虚表(存放虚函数指针的数组)
2.当一个类(B)继承了另外一个类(A)时,类B就会继承A的函数的调用权
所以如果一个基类包含了虚函数,那么其继承类也可以调用这个虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
class B : public A { // 此时类B也拥有自己的虚表
};
类A的虚函数表如下:
1.虚表是一个存放指针的数组,其内元素是虚函数的指针,每个元素对应一个虚函数的函数指针
2.虚表的头目,即虚函数指针的赋值是发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构建出来了。
虚表指针:
1.虚表指针属于类,而不是某个具体的对象,一个类只需要有一个虚表即可。同一个类的所有对象都是使用的同一个虚表。
2.为了指定对象的虚表,对象内部包含了一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*_vpr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
3.验证_vptr的方法(_vptr指针不可被访问),就是求先求一个普通类的字节数大小sizeof(),然后将类中的,某个函数前加上virtual让其变成一个虚函数,再求该类所占字节数的大小,会发现多了4个字节(或者8个字节,取决当前的编译器是64位还是32位),这就验证了_vptr的存在。
#include <iostream>
using namespace std;
class A {
public:
void vfunc1() {
}
void vfunc2() {
}
void func1() {
}
void func2()
{}
private:
int m_data1, m_data2;
};
int main() {
cout << "类A的大小为:" << sizeof(A)<<endl;
}
因为此时类A中没有虚函数 ,只有两个整型的成员变量,所以输出的大小为8
然后将vfun1函数前面加上virtual关键字,让其变成一个虚函数,此时输出的结果变成了16,因为此时编译器为64位,所以指针的大小为8(32位指针对应的大小为4,64位对应的大小为8)
再将vfun2也变成虚函数,输出的大小仍然是16位,说明所以的虚函数都在一个虚函数表中,并不是一个虚函数对应一个虚函数。
5.纯虚函数和抽象类
定义:在基类中声明的虚函数,它在基类中没有函数体的定义,只是通过在函数声明的结尾添加 = 0
来表明其纯虚函数的身份。
语法:virtual 返回值类型 函数名 (参数列表)=0;
当一个类中有了纯虚函数,这个类也被称为抽象类。
抽象类的特点:
1.无法实例化对象
2.子类必须重写抽象类中的纯虚函数,否则也是抽象类。
#include <iostream>
using namespace std;
/*
纯虚函数在虚表中存放的是0
*/
class Animal {
virtual void speak() = 0; // 纯虚函数
void work() { cout << "animal work" << endl; }
};
class Cat : public Animal {
void speak() { cout << "cat speak" << endl; }
void work() { cout << "cat work" << endl; }
};
int main() {
// Animal a;//不能实例化抽象类的对象
Cat c;
return 0;
}
6.虚析构和纯虚析构
虚析构函数:把一个析构函数声明成虚函数,即在析构函数前面加上virtual关键字
纯虚析构:与纯虚函数相似,只不过要有定义(一般在类外定义)
多态在使用使用时,如果子类有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构函数。
解决方案:将父类中的虚构函数改为虚析构或纯虚析构,因为改成了虚函数,此时会根据对象的实际类型,去调用该对象的析构函数。
虚析构和纯析构的共性:
1.可以解决父类指针释放子类的对象
2.都需要有具体的函数实现
虚析构和纯虚析构的区别:
如果是纯虚析构,该类属于抽象类,无法实例化
语法:
虚析构:virtual ~类名{}
纯虚析构:virtual ~类名=0;
#include <iostream>
using namespace std;
class animal {
public:
animal() { cout << "animal构造" << endl; }
virtual ~animal() { cout << "animal析构" << endl; }
};
class cat : public animal {
int* p;
public:
cat():p(new int(10)) { cout << "cat构造" << endl; }
~cat() {
delete p;
cout << "cat析构" << endl;
}
};
int main() {
animal* a = new cat;
delete a;
return 0;
}
总结:
1.虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2.如果子类没有堆区数据,可以不写虚析构或纯虚析构
3.拥有纯虚析构函数的类也是抽象类