多态性
多态性是指操作接口具有表现多种形态的能力:它能够根据操作环境的不同而采用不同的处理方式,或者一组具有相同语义的方法能够在统一接口下为不同的对象服务。
多态性的实现:
绑定机制:绑定是将一个标识符和存储地址联系在一起的过程;
编译时的绑定通过静态绑定来实现:绑定工作在编译链接阶段完成,函数的重载就是静态绑定;
运行时的绑定通过动态绑定来完成:绑定工作在程序运行时完成;
运算符重载的规则
1、 运算符重载是对已有的运算符赋予多重含义,使得它在作用于不同类型的数据时产生不同的作用。
2、 运算符重载只能重载现有的运算符,c++中大部分的运算符都可以被重载,除了下面这几个运算符:“.”、“.*”、“::”、“?:”;
3、 重载之后运算符的优先级和结合性也不会发生改变;
4、 运算符重载是根据新类型数据的需要,对已有的运算符进行修改,运算符重载是通过函数来实现的,根据实际的需要,运算符可以被重载为两种类型:重载为类的非静态成员函数,此时运算符的左操作数是类的对象;有些运算符不能重载为成员函数,例如二元运算符的左操作数不是对象,或者是不能由我们重载运算符的对象,比如系统预定义好的类的对象。当重载为非类的成员函数,也就是全局函数。
运算符重载为类成员函数
重载为类的成员函数定义形式:
函数类型 operator 运算符(形参)
{
......
}
注意:参数个数=原操作数个数-1(后置++、--除外)
双目运算符重载规则
1、 如果要重载B为类成员函数,使之能够实现表达式 oprd1 B oprd2,其中oprd1为A类对象,则B应被重载为A类的成员函数,形参类型应该是oprd2所属的类型。
2、 经重载后,表达式oprd1 B oprd2 相当于oprd1.operator B(oprd2);
案例:重载加法运算符使之能够实现复数的加法
代码:
#include<iostream>
using namespace std;
class Complex
{
public:
Complex() :real(0), img(0){}
Complex(double real, double img) :real(real), img(img){}
Complex operator +(const Complex &c)
{
return Complex(real + c.real, img + c.img);
}
Complex operator -(const Complex &c)
{
return Complex(real - c.real, img - c.img);
}
void show()const { cout << "(" << real << "," << img << ")" << endl; }
private:
double real, img;
};
int main()
{
Complex c1(1, 3), c2(4, 9);
cout << "c1="; c1.show();
cout << "c2="; c2.show();
Complex c3 = c1 + c2;
cout << "c3=c1+c2:" << endl;
cout << "c3="; c3.show();
c3 = c1 - c2;
cout << "c3=c1-c2:" << endl;
cout << "c3="; c3.show();
return 0;
}
单目运算符重载规则
前置单目运算符重载规则
1、 如果要重载 U 为类成员函数,使之能够实现表达式 U oprd,其中 oprd 为A类对象,则 U 应被重载为 A 类的成员函数,无形参;
2、 经重载后,表达式 U oprd 相当于 oprd.operator U()。
后置单目运算符 ++和--重载规则
1、 如果要重载 ++或--为类成员函数,使之能够实现表达式 oprd++ 或 oprd-- ,其中 oprd 为A类对象,则 ++或-- 应被重载为 A 类的成员函数,且具有一个 int 类型形参。
2、 经重载后,表达式 oprd++ 相当于 oprd.operator ++(0)
案例代码
#include<iostream>
using namespace std;
class Clock
{
private:
int hour, minute, second;
public:
Clock() :hour(0), minute(0), second(0){}
Clock(int hour, int minute, int second);
void showTime() const
{
cout << hour << ":" << minute << ":" << second << endl;
}
Clock &operator ++();//前置++自增
Clock operator ++(int);//后置++自增
};
Clock::Clock(int hour, int minute, int second)
{
//时间合法性判断
if (0 <= hour && hour < 24 && 0 <= minute && minute < 60
&& 0 <= second && second < 60)
{
this->hour = hour;
this->minute = minute;
this->second = second;
}
else{ cout << "Time error!" << endl; }
}
Clock &Clock::operator ++()//前置++,无需参数
{
second++;
if (second >= 60)
{
second -= 60;
minute++;
if (minute >= 60)
{
minute -= 60;
hour = (hour + 1) % 24;
}
}
return *this;
}
Clock Clock::operator++(int)//后置++,需要一个参数,用来和前置++形成区别
{
Clock old = *this;
++(*this);//相当于(*this).operator++();通过调用前置++来使得当前Clock自增1
return old;//返回自增1之前的Clock
}
int main()
{
Clock myClock(23, 59, 59);
cout << "First time output: ";
myClock.showTime();
cout << "Show myClock++: ";
(myClock++).showTime();
cout << "Show myColck: ";
myClock.showTime();
cout << "Show ++myClock: ";
(++myClock).showTime();
return 0;
}
运行结果:
First time output: 23:59:59
Show myClock++: 23:59:59
Show myColck: 0:0:0
Show ++myClock: 0:0:1
运算符重载为非成员函数
有些运算符不能重载为成员函数,例如二元运算符的左操作数不是对象,或者是不能由我们重载运算符的对象,比如系统预定义好的类的对象。
运算符重载为非成员函数的规则:
1、 函数的形参代表依自左至右次序排列的各操作数;
2、 重载为非成员函数时,参数个数=原操作数个数(后置++、--除外);并且至少应该有一个自定义类型的参数,不能够所有参数都是基本数据类型,因为基本数据类型的运算是不允许被修改的,比如不能够修改整数加法的规则;
3、 后置单目运算符 ++和--的重载函数,形参列表中要增加一个int,但不必写形参名;
4、 如果在运算符的重载函数中需要操作某类对象的私有成员,可以将此函数声明为该类的友元。
5、 双目运算符 B重载后,表达式oprd1 B oprd2等同于operator B(oprd1,oprd2 );
6、 前置单目运算符 B重载后,表达式 B oprd等同于operator B(oprd );
7、 后置单目运算符 ++和--重载后,表达式 oprd B等同于operator B(oprd,0 )
案例:重载Complex的加减法和“<<”运算符为非成员函数
代码:
#include <iostream>
using namespace std;
class Complex
{
public:
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { }
friend Complex operator+(const Complex &c1, const Complex &c2);
friend Complex operator-(const Complex &c1, const Complex &c2);
friend ostream & operator<<(ostream &out, const Complex &c);
private:
double real; //复数实部
double imag; //复数虚部
};
Complex operator+(const Complex &c1, const Complex &c2)
{
return Complex(c1.real + c2.real, c1.imag + c2.imag);
}
Complex operator-(const Complex &c1, const Complex &c2)
{
return Complex(c1.real - c2.real, c1.imag - c2.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;
}
虚函数
虚函数是用virtual关键字声明的函数。虚函数是实现运行时多态性的基础。c++中的虚函数是动态绑定的函数,也就是在程序编译的过程中,当编译器遇到virtual关键字生命的函数之后,编译器先不去编译这段程序,而是在运行时根据实际的参数类型去选择执行哪一段函数体代码,因此虚函数不能是非静态成员函数,它是属于对象的,而不是属于整个类的;虚函数也不能是内联函数,因为对内联函数的处理是静态的。
什么函数可以是虚函数?
一般的成员函数可以是虚函数,构造函数不能是虚函数,析构函数可以是虚函数。
虚函数的声明:virtual 函数类型 函数名(形参表)
1、 虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
2、 在派生类中可以对基类中的成员函数进行覆盖。
Virtual关键字
1、 派生类可以不显式地用virtual声明虚函数,这时系统就会用以下规则来判断派生类的一个函数成员是不是虚函数:
①该函数是否与基类的虚函数有相同的名称、参数个数及对应参数类型;
②该函数是否与基类的虚函数有相同的返回值或者满足类型兼容规则的指针、引用型的返回值;
2、 如果从名称、参数及返回值三个方面检查之后,派生类的函数满足上述条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数。
3、 派生类中的虚函数还会隐藏基类中同名函数的所有其它重载形式。
4、 一般习惯于在派生类的函数中也使用virtual关键字,以增加程序的可读性。
案例:实现一个通用的打印函数,其能够自动调用不同类的输出函数
代码:
#include<iostream>
using namespace std;
class base1
{
public:
virtual void display();
};
void base1::display(){ cout << "base1:display()" << endl; }
class base2:public base1
{
public:
virtual void display();
};
void base2::display(){ cout << "base2:display()" << endl; }
class base3 :public base1
{
public:
virtual void display();
};
void base3::display(){ cout << "base3:display()" << endl; }
void fun(base1 *p){ p->display(); }//调用display()函数时动态绑定
int main()
{
base1 b1;
base2 b2;
base3 b3;
fun(&b1);
fun(&b2);
fun(&b3);
return 0;
}
运行结果:
base1:display()
base2:display()
base3:display()
请按任意键继续. . .
虚函数是实现动态多态性的基础,当想通过基类的指针调用派生类的函数成员时,就需要使用虚函数来修饰派生类中的同名函数。
虚析构函数
为什么需要虚析构函数?
1、 可能通过基类指针删除派生类对象;
2、 如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),就需要让基类的析构函数成为虚函数,否则执行delete的结果是不确定的。
错误案例:
#include <iostream>
using namespace std;
class Base {
public:
~Base(); //不是虚函数
};
Base::~Base() {
cout<< "Base destructor" << endl;
}
class Derived: public Base{
public:
Derived();
~Derived(); //不是虚函数
private:
int *p;
};
void fun(base *p){delete p; }
int main()
{
base *p=new Derived();//派生类的指针可以隐含的转换为基类的指针
fun(p);
return 0;
}
运行结果:
Base destructor
请按任意键继续. . .
从运行结果可以看到,程序并没有执行Derived类的析构函数,这会造成内存泄漏。改进方法如下:
正确案例:
#include<iostream>
using namespace std;
class base{
public:
virtual ~base();
};
base::~base() {cout << "base destructor" << endl;}
class Derived : public base{
public:
Derived();
virtual ~Derived();
private:
int *p;
};
Derived::Derived(){ p = new int(0); }
Derived::~Derived(){ cout << "Derived destructor" << endl; delete p; }
void fun(base *p){delete p; }
int main()
{
base *p=new Derived();//派生类的指针可以隐含的转换为基类的指针
fun(p);
return 0;
}
运行结果:
Derived destructor
base destructor
请按任意键继续. . .
可以看到,在将析构函数声明为虚函数之后,Derived类的析构函数也被执行了,在Derived类内动态分配的内存也得到了释放。
虚表与动态绑定
前面讲了这么多动态绑定,那动态绑定是如何实现的呢?在运行阶段,操作系统去执行这个可执行程序的时候如何能够知道该执行哪一段函数体呢?这是由于虚表的作用。
虚表
1、 每个多态类有一个虚表(virtual table)
2、 虚表中有当前类的各个虚函数的入口地址
3、 每个对象有一个指向当前类的虚表的指针(虚指针vptr)
动态绑定的实现
1、 构造函数中为对象的虚指针赋值
2、 通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址
3、 通过该入口地址调用虚函数
虚表的示意图如下:
从上面示意图可以看出,类的对象除了我们显示定义的数据成员外,还隐含了一个成员,那就是指向当前类的虚表的指针。
抽象类
带有纯虚函数的类称为抽象类,声明语法为:
class 类名 { virtual 类型 函数名(参数表)=0; //其他成员…… }
什么是纯虚函数呢?
纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:virtual 函数类型 函数名(参数表) = 0
抽象类作用
1、 抽象类为抽象和设计的目的而声明
2、 将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。
3、 对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。
注意:抽象类只能作为基类来使用,不能定义抽象类的对象。
Override与final
Override
多态行为的基础是:基类声明一个虚函数,派生类中声明一个函数来覆盖这个虚函数;
覆盖的要求:函数签名完全一致,函数签名包括函数名、形参列表以及是否是常函数(在函数声明末尾使用const修饰)等;
错误案例:
比如在基类中虚函数定义时有const修饰,而在派生类中定义相同名字函数时却忘记在末尾加上const,那么程序的执行结果可能就不是我们所期望的那样。
C++11中引入了显式覆盖的机制,它能够在编译期间就捕获到这种错误; 在虚函数显式重载中运用,编译器会检查基类是否存在一虚拟函数,与派生类中带有声明override的虚函数(override放在函数声明的末尾),有相同的函数签名(signature);若不存在,则会报错。
final
C++11提供的final,用来避免类被继承,或是基类的函数被改写,
例如:
struct Base1 final { };
struct Derived1 : Base1 { }; // 编译错误:Base1为final,不允许被继承
struct Base2 { virtual void f() final; };
struct Derived2 : Base2 { void f(); // 编译错误:Base2::f 为final,不允许被覆盖 };