目录
1、多态
1.1多态的构成条件
1.2多态的好处
2、虚函数
2.1虚函数重写
2.2虚函数的默认参数
2.3纯虚函数重写
2.4抽象类
2.5虚析构,纯虚析构重写
3、重载、覆盖(重写)、隐藏(重定义)的对比
编辑
多态是c++面向对象三大特性之一
程序调用函数时,将使用哪个可执行代码块呢?编译器负责回答这个问题。将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。在c语言中,在非常简单,因为每个函数名都对应一个不同的函数。在c++中,由于函数重载的缘故,这项任务更复杂。编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而,c/c++编译可以在编译过程完成这种联编。在编译过程中进行联编被称为给静态联编,又称为早期联编,然而虚函数使这项工作变得更困难。编译器必须生成能够在程序运行时选择正确使虚方法的代码,这被称为动态联编,又称为晚期联编。
1、多态
多态就是函数调用的多种形态,使用多态能够使得不同的对象去完成同一件事时,产生不同的动作和结果。
多态分为静态多态和动态多态
(1)静态多态,也成为静态绑定或前期绑定(早绑定):函数重载和运算符重载就属于静态多态。静态多态也成为编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果又对应的函数就调用该甘薯,否则出现编译错误
(2)动态多态,也成为动态绑定或后期绑定(晚绑定):在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,即运行时的多态。在程序执行期间(非编译期)判断所英勇的实际类型,根据其实际类型调用相应的方法。
父类指针或引用指向父类,调用的就是父类的虚函数
父类指针或引用指向子类,调用的就是子类的虚函数
静态多态和动态多态区别:
静态多态的函数地址早绑定 - 编译阶段确定函数地址
动态多态的函数地址晚绑定 - 运行阶段确定函数地址
1.1多态的构成条件
同一操作作用于不同的对象,可以有不同的执行结果,产生不同的执行结果,这就是多态性。简单的说,就是用基类的指针指向子类的对象
所以在继承中要想构成多态需要满足两个条件:
1、必须通过基类的指针或者引用指向子类的对象
2、被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
1.2多态的好处
多态的优点:
1、代码组织结构清晰
2、可读性强
3、利于前期和后期的扩展以及维护
2、虚函数
被virtual修饰的类成员函数被称作虚函数
1、只有类的非静态成员函数前可以加virtual,普通函数前不能加virtual
2、虚函数的virtual和虚继承的vittual是同一个关键字,但实际上并没有任何关系。虚函数的virtual是为了实现多态,而虚继承的virtual是为了解决菱形继承的数据冗余和二义性
2.1虚函数重写
虚函数的重写也叫做虚函数的覆盖,若派生类中有一个和基类完全相同的虚函数(返回值类型相同,函数名相同以及参数列表完全相同),这种称为派生类的虚函数重写了基类的虚函数。
通过基类的指针或引用子类对象,从而调用我们写的虚函数,此时根据不同类型的对象,调用的就是不同的函数,产生的也是不同的结果,进而实现了动态多态。
调用哪个类型的虚函数,取决于基类指针指向或引用的对象是哪种类型的对象。
如果不使用基类的指针或引用去调用虚函数,则只会调用基类的虚函数。
#include <iostream>
using namespace std;
class Animal
{
public:
//Speak函数就是虚函数
//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat :public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
class Dog :public Animal
{
public:
void speak()
{
cout << "小狗在说话" << endl;
}
};
void DoSpeak(Animal & animal)
{
animal.speak();
}
void test01()
{
Cat cat;
DoSpeak(cat);
Dog dog;
DoSpeak(dog);
}
int main() {
test01();
return 0;
}
注意:在重写基类虚函数时,派生类的虚函数不加virtual关键字也可以进行动态多态,主要原因是因为继承后基类的虚函数被继承下来了,在派生类中依旧保持虚函数属性。但是这种写法不是很规范,因为派生类也有可能会被继承,为了区分虚函数,建议在派生类的虚函数前也加上virtual关键字。
2.2虚函数的默认参数
动态多态中,虚函数默认参数,调用者是哪个类,就用对应类中的函数的默认参数,如果不是使用默认参数,那么就会用传递进去的参数
参数值的优先顺序:传递进去的参数 > 基类的默认参数 > 派生的默认参数
测试代码如下:
#include <iostream>
using namespace std;
class Base
{
public:
Base(){cout<<"Base()"<<endl;}
~Base(){cout<<"~Base()"<<endl;}
virtual void show(int a=123) //基类的虚函数带默认参数
{
cout<<"Base show"<<a<<endl;
}
private:
};
//派生类
class Child:public Base{
public:
Child(){cout<<"Child()"<<endl;}
~Child(){cout<<"~Child()"<<endl;}
void show(int a=456) //派生类也带默认参数
{
cout<<"Child show"<<a<<endl;
}
private:
};
int main()
{
//派生类访问自己的成员 不是多态
Child mya;
//mya.show();//Child show456
//多态中,虚函数默认参数,调用者是哪个类,就用对应类中的函数的默认参数
Base *d = &mya;//Child show123
d->show();
//如果不是使用默认参数,那么就会用传递进去的参数
//参数值得优先顺序 传递进去的参数 > 基类的默认参数 >派生类的默认参数
// d->show(1000);
return 0;
}
2.3纯虚函数重写
在堕胎中,通常父类的虚函数的实现是某无意义的,主要都是调用了子类重写的内容,那我们就可以将虚函数写成纯虚函数。c++通过使用纯虚函数提供未实现的函数。
纯虚函数的格式:
virtual 函数返回类型 函数名(参数表) = 0;
在虚函数声明的时候直接赋值为0,这样虚函数就变成纯虚函数了
什么时候下使用纯虚函数
在基类本身生成对象时不合理的时候,比如动物作为一个基类派生出老虎等子类,但动物本身这基类直接生成对象时不合理的,所以为解决这问题,方便使用类的多态性,引入了纯虚函数的概念,将函数定义为纯虚函数,则编译器要求在派生类中必须要重新给纯虚函数以实现多态性
纯虚函数并不需要实现,如果一个类中有纯虚函数那么这个类就是抽象类,如果派生类没有把基类的纯虚函数全部实现,那么派生类还是抽象类。
2.4抽象类
在类中包含纯虚函数,那么一个类中有纯虚函数那么这个类就是抽象类
抽象类的特点:
1、无法实例化对象
2、子类必须重写抽象类中的纯虚函数,否则也属于抽象类
实现继承: 普通函数的继承是一种实现继承,派生类继承了基类函数的实现,可以使用该函数。
接口继承: 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。
建议: 所以如果不实现多态,就不要把函数定义成虚函数。
2.5虚析构,纯虚析构重写
我们有时会让一个基类指针指向用 new 运算符动态生成的派生类对象;同时,用 new 运算符动态生成的对象都是通过 delete 指向它的指针来释放的。如果一个基类指针指向用 new 运算符动态生成的派生类对象,而释放该对象时是通过释放该基类指针来完成的,就会导致delete的时候只会调用基类的析构函数,而不会调用派生类的析构函数,如果派生类的析构函数中有释放成员内存空间,可能会造成内存泄漏。所以C++ 规定,需要将基类的析构函数声明为虚函数,即虚析构函数。只要基类的析构函数是虚函数,那么派生类的析构函数不论是否用virtual关键字声明,都自动成为虚析构函数。一般来说,一个类如果定义了虚函数,则最好将析构函数也定义成虚函数。
为什么要需要虚析构函数呢?
动态多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,会造成内存泄漏,解决这个问题就需要用到虚析构函数。
虚析构函数的格式:
virtual ~类名(){}
1、虚析构函数就是用来解决通过父类指针释放子类对象。
2、如果子类中没有堆区数据,可以不写虚析构函数
纯虚函数的格式
virtual ~类名() = 0; //类内定义
类名::~类名(){}` //类外声明
和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是抽象类,不能被实例化,测试代码如下:
#include<iostream>
#include <unistd.h>
#include<cstring>
using namespace std;
//基类 --动物类
class Animal{
public:
Animal(){cout<<"Animal()"<<endl;}
//将基类的析构函数声明为 虚析构函数
//作用:当使用基类的指针销毁 派生类对象的时候,让派生类对象的析构函数也执行
virtual ~Animal(){cout<<"~Animal()"<<endl;} //常用方法---派生类里面是否有指针
//可以把基类的虚析构函数 声明定义为 纯虚析构函数
//纯虚析构 函数必须在类内声明 类外实现
//virtual ~Animal() = 0;
//~Animal();
//行为
//如何让基类 不能实例化对象
//有两种方法:第一种 将虚函数声明定义为纯虚函数
// 第二种方法将虚析构函数声明定义为纯析构函数,但是纯析构函数必须在类外实现
// virtual void speak(){
// cout<<"Animal::speak"<<endl;
// }
virtual void speak() = 0;
};
// Animal::~Animal()
// {
// cout<<"~Animal()"<<endl;
// }
//派生类 狗
class Dog:public Animal
{
public:
Dog(const char*name = "旺财"){
cout<<"Dog()"<<endl;
d_name = new char[strlen(name) + 1];
strcpy(this->d_name,name);
}
//当基类的析构函数声明为 虚析构的时候,派生类的析构函数也默认会加上关键字 virtual
// ~Dog()
virtual ~Dog()
{
cout<<"~Dog()"<<endl;
//在析构函数中 释放 指针成员 指向的堆空间
delete []this->d_name;
}
//派生类 中 实现 基类的 虚函数
virtual void speak(){
cout<<"Dog::speak"<<endl;
}
private:
char *d_name;
};
int main()
{
//Animal *p = new Dog;
//通过基类指针指向 派生类对象
//p->speak();
//通过基类指针 释放 对象的内存空间,默认只会调用 基类的析构函数
//delete p; //虚拟析构不仅释放了基类的new空间,也释放了派生类的new空间
//动物类 是 基类 ,实例化 对象 不合理
//因为基类中有纯虚函数(纯虚析构函数),所以该类是抽象类,抽象类不能实例化
//抽象类:只能通过继承 在子类中 重写纯虚函数
Animal p1;
//p1.speak();
return 0;
}
//将基类的析构函数声明为 虚析构函数
/作用:当使用基类的指针销毁 派生类对象的时候,让派生类对象的析构函数也执行
//可以把基类的虚析构函数 声明定义为 纯虚析构函
//纯虚析构 函数必须在类内声明 类外实现
//如何让基类 不能实例化对象
//有两种方法:第一种 将虚函数声明定义为纯虚函数
// 第二种方法将虚析构函数声明定义为纯析构函数,但是纯析构函数必须在类外实现