8.1 多态性概念
8.1.1 多态的类型
- 专用多态
- 重载多态
- 强制多态
- 通用多态
- 包含多台
- 参数多态
8.1.2 多态的实现
编译时的多态:编译的过程中确定了同名操作的具体操作对多
运行时的多态:在程序运行过程中才动态地确定操作所针对的具体对象
绑定是指计算机程序自身彼此关联的过程,也就是把一个标识符名和一个存储地址联系在一起的过程;
用面向对象的术语讲,就是把一条消息和一个对象的方法相结合的过程。
静态绑定:绑定工作在编译连接阶段完成的情况称为静态绑定
动态绑定:绑定工作在程序运行阶段完成的情况称为动态绑定
8.2 运算符重载
8.2.1 运算符重载的规则
- 运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时导致不同的行为。
- C++ 几乎可以重载全部的运算符,而且只能够重载C++中已经有的。
- 不能重载的运算符:“.”、“.*”、“::”、“?:”
- 重载之后运算符的优先级和结合性都不会改变。
- 运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。例如:
- 使复数类的对象可以用“+”运算符实现加法;
- 是时钟类对象可以用“++”运算符实现时间增加1秒。
- 重载为类的非静态成员函数;
- 重载为非成员函数。
运算符重载为类的成员函数的一般语法形式为:
返回类型 operator 运算符(形参表)
{
函数体
}
运算符重载为非成员函数的一般语法形式为:
返回类型 operator 运算符(形参表)
{
函数体
}
运算符重载的主要优点:就是可以改变现有运算符的操作方式,以用于类类型,使得程序看起来更加直观。
8.2.2 运算符重载为成员函数
函数类型 operator 运算符(形参)
{
......
}
参数个数=原操作数个数-1 (后置++、--除外)
1、双目运算符重载规则
- 如果要重载 B 为类成员函数,使之能够实现表达式 oprd1 B oprd2,其中 oprd1 为A 类对象,则 B 应被重载为 A 类的成员函数,形参类型应该是 oprd2 所属的类型。
- 经重载后,表达式 oprd1 B oprd2 相当于 oprd1.operator B(oprd2)
例8-1复数类加减法运算重载为成员函数
- 要求:
- 将+、-运算重载为复数类的成员函数。
- 规则:
- 实部和虚部分别相加减。
- 操作数:
- 两个操作数都是复数类的对象。
- 源代码:
#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; //复数虚部
};
//例8-1复数类加减法运算重载为成员函数
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;
}
//例8-1复数类加减法运算重载为成员函数
int main() {
Complex c1(5, 4), c2(2, 10), c3;
cout << "c1 = "; c1.display();
cout << "c2 = "; c2.display();
c3 = c1 - c2; //使用重载运算符完成复数减法
cout << "c3 = c1 - c2 = "; c3.display();
c3 = c1 + c2; //使用重载运算符完成复数加法
cout << "c3 = c1 + c2 = "; c3.display();
return 0;
}
2、单目运算符重载为成员函数
前置单目运算符重载规则
- 如果要重载 U 为类成员函数,使之能够实现表达式 U oprd,其中 oprd 为A类对象,则 U 应被重载为 A 类的成员函数,无形参。
- 经重载后,表达式 U oprd 相当于 oprd.operator U()
后置单目运算符 ++和--重载规则
- 如果要重载 ++或--为类成员函数,使之能够实现表达式 oprd++ 或 oprd-- ,其中 oprd 为A类对象,则 ++或-- 应被重载为 A 类的成员函数,且具有一个 int 类型形参。
- 经重载后,表达式 oprd++ 相当于 oprd.operator ++(0)
例:重载前置++和后置++为时钟类成员函数
- 前置单目运算符,重载函数没有形参
- 后置++运算符,重载函数需要有一个int形参
- 操作数是时钟类的对象。
- 实现时间增加1秒钟。
#include <iostream>
using namespace std;
class Clock {//时钟类定义
public:
Clock(int hour = 0, int minute = 0, int second = 0);
void showTime() const;
//前置单目运算符重载
Clock& operator ++ ();
//后置单目运算符重载
Clock operator ++ (int);
private:
int hour, minute, second;
};
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;
}
void Clock::showTime() const { //显示时间
cout << hour << ":" << minute << ":" << second << endl;
}
//例8-2重载前置++和后置++为时钟类成员函数
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); //调用前置“++”运算符
return old;
}
//例8-2重载前置++和后置++为时钟类成员函数
int main() {
Clock myClock(23, 59, 59);
cout << "First time output: ";
myClock.showTime();
cout << "Show myClock++: ";
(myClock++).showTime();
cout << "Show ++myClock: ";
(++myClock).showTime();
return 0;
}
8.2.3 运算符重载为非成员函数
运算符重载为非成员函数
有些运算符不能重载为成员函数,例如二元运算符的左操作数不是对象,或者是不能由我们重载运算符的对象
运算符重载为非成员函数的规则
- 函数的形参代表依自左至右次序排列的各操作数。
- 重载为非成员函数时
- 参数个数=原操作数个数(后置++、--除外)
- 至少应该有一个自定义类型的参数。
- 后置单目运算符 ++和--的重载函数,形参列表中要增加一个int,但不必写形参名。
- 如果在运算符的重载函数中需要操作某类对象的私有成员,可以将此函数声明为该类的友元。
运算符重载为非成员函数的规则
- 双目运算符 B重载后,
- 表达式oprd1 B oprd2
- 等同于operator B(oprd1,oprd2 )
- 前置单目运算符 B重载后,
- 表达式 B oprd
- 等同于operator B(oprd )
- 后置单目运算符 ++和--重载后,
- 表达式 oprd B
- 等同于operator B(oprd,0 )
例:重载Complex的加减法和“<<”运算符为非成员函数
将+、-(双目)重载为非成员函数,并将其声明为复数类的友元,两个操作数都是复数类的常引用。 • 将<<(双目)重载为非成员函数,并将其声明为复数类的友元,它的左操作数是std::ostream引用,右操作数为复数类的常引用,返回std::ostream引用,用以支持下面形式的输出:
cout << a << b;
该输出调用的是:
operator << (operator << (cout, a), b);
//8_3.cpp
#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;
}
8.3虚函数
初识虚函数
- 用virtual关键字说明的函数
- 虚函数是实现运行时多态性基础
- C++中的虚函数是动态绑定的函数
- 虚函数必须是非静态的成员函数,虚函数经过派生之后,就可以实现运行过程中的多态。
- 一般成员函数可以是虚函数
- 构造函数不能是虚函数
- 析构函数可以是虚函数
8.3.1 一般虚函数成员
-
虚函数的声明
virtual 函数类型 函数名(形参表);
-
虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
- 在派生类中可以对基类中的成员函数进行覆盖。
- 虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的。
virtual 关键字
- 派生类可以不显式地用virtual声明虚函数,这时系统就会用以下规则来判断派生类的一个函数成员是不是虚函数:
- 该函数是否与基类的虚函数有相同的名称、参数个数及对应参数类型;
- 该函数是否与基类的虚函数有相同的返回值或者满足类型兼容规则的指针、引用型的返回值;
- 如果从名称、参数及返回值三个方面检查之后,派生类的函数满足上述条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数。
- 派生类中的虚函数还会隐藏基类中同名函数的所有其它重载形式。
- 一般习惯于在派生类的函数中也使用virtual关键字,以增加程序的可读性。
例7-3 类型转换规则举例
#include <iostream>
using namespace std;
class Base1 { //基类Base1定义
public:
void display() const {
cout << "Base1::display()" << endl;
}
};
class Base2: public Base1 { //公有派生类Base2定义
public:
void display() const {
cout << "Base2::display()" << endl;
}
};
class Derived: public Base2 { //公有派生类Derived定义
public:
void 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;
}
例8-4通过虚函数实现运行时多态
现在改进一下第7章的程序
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void display() const; //虚函数
};
void Base1::display() const {
cout << "Base1::display()" << endl;
}
class Base2::public Base1 {
public:
virtual void display() const;
};
void Base2::display() const {
cout << "Base2::display()" << endl;
}
class Derived: public Base2 {
public:
virtual void display() const;
};
void Derived::display() const {
cout << "Derived::display()" << endl;
}
void fun(Base1 *ptr) {
ptr->display();
}
int main() {
Base1 base1;
Base2 base2;
Derived derived;
fun(&base1);
fun(&base2);
fun(&derived);
return 0;
}
8.3.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;
};
#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;
};
虚表与动态绑定
虚表
- 每个多态类有一个虚表(virtual table)
- 虚表中有当前类的各个虚函数的入口地址
- 每个对象有一个指向当前类的虚表的指针(虚指针vptr)
动态绑定的实现
- 构造函数中为对象的虚指针赋值
- 通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址
- 通过该入口地址调用虚函数
虚表示意图
8.4 纯虚函数与抽象类
8.4.1 纯虚函数
纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:
virtual 函数类型 函数名(参数表) = 0; (0表示没有函数体)
带有纯虚函数的类称为抽象类
8.4.2 抽象类
- 带有纯虚函数的类称为抽象类:
class 类名
{
virtual 类型 函数名(参数表)=0;
//其他成员……
}
抽象类作用
- 抽象类为抽象和设计的目的而声明
- 将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。
- 对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。
注意
- 抽象类只能作为基类来使用。
- 不能定义抽象类的对象。
例:
//8_6.cpp
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void display() const = 0; //纯虚函数 Base是抽象类,无法产生对象了
};
class Base2: public Base1 {
public:
virtual void display() const; //覆盖基类的虚函数
};
void Base2::display() const {
cout << "Base2::display()" << endl;
}
class Derived: public Base2 {
public:
virtual void display() const; //覆盖基类的虚函数
};
void Derived::display() const {
cout << "Derived::display()" << endl;
}
void fun(Base1 *ptr) {
ptr->display();
}
int main() {
Base2 base2;
Derived derived;
fun(&base2);
fun(&derived);
return 0;
}
C++11:override 与 final
- 多态行为的基础:基类声明虚函数,继承类声明一个函数覆盖该虚函数
- 覆盖要求: 函数签名(signatture)完全一致
- 函数签名包括:函数名 参数列表 const
下列程序就仅仅因为疏忽漏写了const,导致多态行为没有如期进行
显式函数覆盖
- C++11 引入显式函数覆盖,在编译期而非运行期捕获此类错误。
- 在虚函数显式重载中运用,编译器会检查基类是否存在一虚拟函数,与派生类中带有声明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,不允许被覆盖
};