提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
一、多态性概述
多态是指同样的消息被不同类型的对象接收时导致不同的行为,所谓消息是指对类的成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数。多态是指一段程序能够处理多种类型对象的能力
1、多态的类型
多态可以分为四类,重载多态、强制多态、包含多态、参数多态。
- 强制多态——强制转换
- 重载多态——给同一个名字赋予不同的含义,如函数重载、运算符重载
- 类型参数化多态——通过模板来实现,函数模板、类模板
- 包含多态——通过虚函数来实现
2、多态的实现
多态从实现的角度来讲,可以分为两类:编译时的多态和运行时的多态。前者是在编译过程中确定了同名操作的具体操作对象,后者是在程序运行过程中才动态地确定操作所针对的具体对象。这种确定操作的具体对象的过程就是绑定。绑定是指计算机程序自身彼此关联的过程,也就是把一个标识符和一个存储地址联系在一起的过程。
按照绑定进行的阶段不同,可以分为两种不同的绑定方法:静态绑定和动态绑定。绑定工作在编译连接阶段完成的情况称为静态绑定;绑定工作在程序运行阶段完成的情况称为动态绑定。
二、运算符重载
运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时导致不同的行为。
1、运算符重载的规则
- 重载之后运算符的优先级和结合性都不会改变。
- 运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。
- C++运算符除了少数几个,全部都可以重载,且只能重载C++中以及存在的运算符。
C++中不能重载的运算符有:类属关系运算符“ . ”、成员指针运算符“ .* ”、作用域分辨符“ :: ”、三目运算符“ ? : ”。
运算符重载形式有两种:
//重载为类的成员函数
返回类型 类名::operator 运算符(形参表)
{
函数表
}
//重载为非成员函数
返回类型 operator 运算符(形参表)
{
函数表
}
返回类型指定了重载运算符的返回类型,也就是运算结果类型;operator是定义运算符重载函数的关键字;运算符即是要重载的运算符名称;形参表中给出重载运算符所需要的参数和类型。
2、运算符重载为成员函数
对于双目运算符B,如果要重载为类的成员函数,实现oprd1 B oprd2,oprd1为A类的对象,则应当把运算符B重载为A类的成员函数,该函数只有一个形参,形参类型是oprd2所属的类型。结果重载之后,表达式oprd1 B oprd2 就相对于函数调用了oprd1.operator B(oprd2).
对于单目运算符U,如“-”(负号)等,如果要重载为类的成员函数,实现 U oprd1,oprd为A类的对象,则U应当重载为A类的成员函数,该函数没有形参,就相当于调用了oprd.operator U()。
对于后置运算符++和–,重载为A类的成员函数,这是函数要带有一个整形(int)形参。表达式oprd++就相当于调用了oprd.operator。这里的int类型参数不起任何作用,只是用于区分前置++、–和后置++、–。
#include <iostream>
using namespace std;
class Complex { //复数类定义
public: //外部接口
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { } //构造函数
Complex operator + (const Complex &c2) const; //运算符+重载成员函数
Complex operator - (const Complex &c2) const; //运算符-重载成员函数
void display() const; //输出复数
private: //私有数据成员
double real; //复数实部
double imag; //复数虚部
};
Complex Complex::operator + (const Complex &c2) const { //重载运算符函数实现
return Complex(real + c2.real, imag + c2.imag); //创建一个临时无名对象作为返回值
}
Complex Complex::operator - (const Complex &c2) const { //重载运算符函数实现
return Complex(real - c2.real, imag - c2.imag); //创建一个临时无名对象作为返回值
}
void Complex::display() const {
cout << "(" << real << ", " << imag << ")" << endl;
}
int main() { //主函数
Complex c1(5, 4), c2(2, 10), c3; //定义复数类的对象
cout << "c1 = "; c1.display();
cout << "c2 = "; c2.display();
c3 = c1 - c2; //使用重载运算符完成复数减法 c3=c1.operator -(c2)
cout << "c3 = c1 - c2 = "; c3.display();
c3 = c1 + c2; //使用重载运算符完成复数加法
cout << "c3 = c1 + c2 = "; c3.display();
return 0;
}
3、运算符重载为非成员函数
上面我们发现,当运算符重载为类的成员函数时,函数的参数个数比原来的操作数个数要少一个;当重载为非成员函数时,参数个数和原操作数相同。
因为是非成员函数,有时候需要访问参数对象的私有成员,这时候可以声明为类的友元函数。
class Complex { //复数类定义
public: //外部接口
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { } //构造函数
friend Complex operator + (const Complex &cc1, const Complex &cc2); //运算符+重载
friend Complex operator - (const Complex &cc1, const Complex &cc2); //运算符-重载
friend ostream & operator << (ostream &out, const Complex &c); //运算符<<重载
private: //私有数据成员
double real; //复数实部
double imag; //复数虚部
};
Complex operator + (const Complex &cc1, const Complex &cc2) { //重载运算符函数实现
return Complex(cc1.real + cc2.real, cc1.imag + cc2.imag);
}
Complex operator - (const Complex &cc1, const Complex &cc2) { //重载运算符函数实现
return Complex(cc1.real - cc2.real, cc1.imag - cc2.imag);
}
ostream & operator << (ostream &out, const Complex &c) { //重载运算符函数实现
out << "(" << c.real << ", " << c.imag << ")";
return out;
}
int main() { //主函数
Complex c1(5, 4), c2(2, 10), c3; //定义复数类的对象
cout << "c1 = " << c1 << endl;
cout << "c2 = " << c2 << endl;
c3 = c1 - c2; //使用重载运算符完成复数减法
cout << "c3 = c1 - c2 = " << c3 << endl;
c3 = c1 + c2; //使用重载运算符完成复数加法
cout << "c3 = c1 + c2 = " << c3 << endl;
return 0;
}
//输出结果:
c1 = (5, 4)
c2 = (2, 10)
c3 = c1 - c2 = (3, -6)
c3 = c1 + c2 = (7, 14)
三、虚函数
虚函数是动态绑定的基础。虚函数必须是非静态从成员函数。虚函数结果派生之后,在类族中就可以实现运行过程中的多态。
如果需要通过基类的指针指向派生类的对象,并访问某个与基类同名的成员,那么首先在基类中将这个同名函数说明为虚函数。这样,通过基类类型的指针,就可以使属于不同派生类的不同对象产生不同的行为,从而实现了运行过程的多态。(也就是可以在子类重写父类的函数)
1、一般虚函数成员
语法:
virtual 函数类型 函数名(形参表);
- 虚函数声明只能出现在类定义中的函数原型声明,而不能在成员函数实现的时候。
- 运行过程中的多态有3个条件:满足赋值兼容规则、要声明虚函数、由成员函数来调用或者是通过指针或引用来访问虚函数。
#include <iostream>
using namespace std;
class Base1 { //基类Base1定义
public:
virtual void display() const; //虚函数
};
void Base1::display() const {
cout << "Base1::display()" << endl;
}
class Base2: public Base1 { //公有派生类Base2定义
public:
void display() const; //覆盖基类的虚函数
};
void Base2::display() const {
cout << "Base2::display()" << endl;
}
class Derived: public Base2 { //公有派生类Derived定义
public:
void display() const; //覆盖基类的虚函数
};
void Derived::display() const {
cout << "Derived::display()" << endl;
}
void fun(Base1 *ptr) { //参数为指向基类对象的指针
ptr->display(); //"对象指针->成员名"
}
int main() { //主函数
Base1 base1; //定义Base1类对象
Base2 base2; //定义Base2类对象
Derived derived; //定义Derived类对象
fun(&base1); //用Base1对象的指针调用fun函数
fun(&base2); //用Base2对象的指针调用fun函数
fun(&derived); //用Derived对象的指针调用fun函数
return 0;
}
在上面的程序中,派生类没有显式给出虚函数声明,这时系统就会遵循以下规则来判断派生类的一个函数成员是不是虚函数。
- 该函数是否与基类的虚函数有相同的名称。
- 该函数是否与基类的虚函数有相同的参数个数及相同的对应参数类型。
- 该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容规则的指针、引用型的返回值。
派生类的函数满足了上述条件就会自动确定为虚函数。这时候,派生类的虚函数就会覆盖了基类的虚函数,也会隐藏基类中同名函数的所有其他重载形式。
final和override说明符
override
为了避免因为弄错了派生类函数的参数表,导致不能覆盖基类中的虚函数。就可以使用override关键字来说明派生类中的虚函数。如果使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,这时就会报错。
class Base {
public:
virtual void f1(int) const;
virtual void f2();
void f3();
};
class Derived :public Base{
public:
void f1(int) const override;//正确,f1与基类的f1匹配
void f2(int) override;//错误,参数表错误
void f3() override;//错误,基类的f3不是虚函数
void f4() override;//错误,基类没有f4函数
};
final
相应的如果把某个函数指定为final,意味着该函数不能被覆盖,任何试图覆盖该函数的操作都将引发错误。
class Base {
public:
virtual void f1(int) const final;
virtual void f2();
void f3();
};
class Derived :public Base{
public:
void f1(int) const override;//错误,
};
2、虚析构函数
语法:
virtual ~类名();
如果一个类的析构函数是虚函数,那么,由它派生而来的所有子类的析构函数也是虚构函数。析构函数设置为虚函数之后,在使用指针引用时可以动态绑定,实现运行时的多态,保证使用基类类型的指针就能够调用适当的析构函数针对不同的对象进行清理工作。
简单来说,如果肯通过基类指针调用对象的析构函数(通过delete),就需要让基类的析构函数成为虚函数,否则会产生不确定的后果。
#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base();
//~Base();
};
Base::~Base() {
cout<< "Base destructor" << endl;
}
class Derived: public Base {
public:
Derived();
virtual ~Derived();
//~Derived();
private:
int *p;
};
Derived::Derived() {
p = new int(0);
}
Derived::~Derived() {
cout << "Derived destructor" << endl;
delete p;
}
void fun(Base* b) {
delete b;
}
int main() {
Base *b = new Derived();
fun(b);
return 0;
}
//输出结果
Derived destructor
Base destructor
我们用基类Base的指针指向了一个Derived成员,如果不使用虚析构函数,只会调用Base的析构函数,这时Derived的 p 所指向的内存可见就没有得到释放,造成了内存泄漏。使用虚析构函数就可以让派生类的析构函数也能被调用,派生类对象中动态申请的内存空间被正确地释放了。
纯虚函数与抽象类
1、纯虚函数
纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数声明格式如下:
virtual 函数类型 函数名(参数表)= 0;
声明为纯虚函数之后,基类中就可以不再给出函数的实现部分。
2、抽象类
带有纯虚函数的类是抽象类。抽象类的主要作用是通过它为一个类族建立一个公共的接口,使他们能够更有效地发挥多态特性。抽象类声明了一族派生类的公共接口,而接口的完整实现,即虚函数的函数体,要由派生类自己定义。
- 如果派生类没有给出全部纯虚函数的实现,这个派生类仍然是一个抽象类,不可用定义自己的对象。
- 抽象类不能实例化,但我们可以定义一个抽象类的指针和引用。通过指针和引用,就可以指向并访问派生类对象,进而访问派生类的成员,这种访问是具有多态性的。
#include <iostream>
using namespace std;
class Base1 { //基类Base1定义
public:
virtual void display() const = 0; //纯虚函数
};
class Base2: public Base1 { //公有派生类Base2定义
public:
void display() const; //覆盖基类的虚函数
};
void Base2::display() const {
cout << "Base2::display()" << endl;
}
class Derived: public Base2 { //公有派生类Derived定义
public:
void display() const; //覆盖基类的虚函数
};
void Derived::display() const {
cout << "Derived::display()" << endl;
}
void fun(Base1 *ptr) { //参数为指向基类对象的指针
ptr->display(); //"对象指针->成员名"
}
int main() { //主函数
Base2 base2; //定义Base2类对象
Derived derived; //定义Derived类对象
fun(&base2); //用Base2对象的指针调用fun函数
fun(&derived); //用Derived对象的指针调用fun函数
return 0;
}
//运行结果:
Base2::display()
Derived::display()