多态性
运算符重载的规则
-
运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时导致不同的行为。
-
不能重载的运算符:“.”、“.*”、“::”、“?:”
- 重载之后运算符的优先级和结合性都不会改变。
-
运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。例如:
- 使复数类的对象可以用“+”运算符实现加法;
- 使时钟类对象可以用“++”运算符实现时间增加1秒。
-
重载为类的非静态成员函数;
-
重载为非成员函数。
双目运算符重载为成员函数
重载为类成员的运算符函数定义形式
函数类型 operator 运算符(形参)
{
......
}
参数个数=原操作数个数-1 (后置++、--除外)
双目运算符重载规则
- 如果要重载
B
为类成员函数,使之能够实现表达式oprd1 B oprd2
,其中oprd1
为A 类对象,则B
应被重载为 A 类的成员函数,形参类型应该是oprd2
所属的类型。 - 经重载后,表达式
oprd1 B oprd2
相当于oprd1.operator B(oprd2)
举例:复数类加减法运算重载为成员函数
-
要求:
- 将+、-运算重载为复数类的成员函数。
-
规则:
- 实部和虚部分别相加减。
-
操作数:
- 两个操作数都是复数类的对象。
-
源代码:
#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; }
单目运算符重载为成员函数
前置单目运算符重载规则
- 如果要重载
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; }
运算符重载为非成员函数
- 有些运算符不能重载为成员函数,例如二元运算符的左操作数不是对象,或者是不能由我们重载运算符的对象
运算符重载为非成员函数的规则
- 函数的形参代表依自左至右次序排列的各操作数。
- 重载为非成员函数时
- 参数个数=原操作数个数(后置++、–除外)
- 至少应该有一个自定义类型的参数。
- 后置单目运算符 ++和–的重载函数,形参列表中要增加一个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);
源代码:
#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; }
虚函数
- 虚基类解决的是类成员标识二义性和信息冗余的问题。
- 虚函数是实现动态多样性的基础。
为什么要有虚函数?
首先,从基类指针可以指向的实际对象说起。基类指针可以指向本类以及派生类的实例对象,就如同哺乳动物
这个类的指针可以指向指向其派生类人类
的某一个对象张三
,还以指向其派生类大象类
的某一个对象001eleph
,但是这些对象的很多行为是不一样的,比如说跑
这一动作就不一样。所以我们在用基类指针调用某一未知对象的特定行为的时候,这是不可能确定的。如果非要立即确定,也就是静态绑定,就是说在编译阶段就把未知对象的特定动作确定下来,那就索性是编译器只能把声明指针的基类的该动作绑定进去(假设基类的该动作有声明和定义)。然而这并不符合我们的设计意图,不符合多态的真实含义。所以就出现了动态绑定,也就是在实际运行时,根据主调函数真实传递了什么最近类的实例对象给形参指针或引用,再确定该对象的特定行为是什么。
编译器难道看不出我写的代码到底想要给形参传递什么具体的最近类的对象吗?是的,编译器不可能明白你的意图,也就是编译器没有那么智能,他不可能把整个代码读懂,分析出你这个基类指针到底是指向哪个类的亲生对象,编译器只会把各个模块编译后,最后链接起来。
还有一点很重要,就是很可能我们是分别编译,最后链接生成的可执行文件,基于这种可能,就算编译器有了些许的智能,也没办法获悉他要链接的模块到底符不符合调用者真正想要调用的成员函数的意图。
类赋值的兼容性
公有类派生时,任何基类对象可以出现的位置都可以使用派生类对象代替
- 派生类对象赋值给基类对象,仅赋值基类部分
- 用派生类对象初始化基类引用,仅操作基类部分
- 使基类的指针指向派生类对象,仅解引用基类部分
一般虚函数
初识虚函数
- 用
virtual
关键字说明的函数 - 虚函数是实现运行时多态性基础
- C++中的虚函数是动态绑定的函数
- 虚函数必须是非静态的成员函数,虚函数经过派生之后,就可以实现运行过程中的多态。
- 一般成员函数可以是虚函数
- 构造函数不能是虚函数(因为如果构造函数是虚函数,则由于无法找到构造函数入口而无法实例化该类的对象)
- 析构函数可以是虚函数
一般虚函数成员
-
虚函数的声明
virtual 函数类型 函数名(形参表);
-
虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
-
在派生类中可以对基类中的成员函数进行覆盖。
-
虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态 的。
virtual 关键字
- 派生类可以不显式地用
virtual
声明虚函数,这时系统就会用以下规则来判断派生类的一个函数成员是不是虚函数:- 该函数是否与基类的虚函数有相同的名称、参数个数及对应参数类型;
- 该函数是否与基类的虚函数有相同的返回值或者满足类型兼容规则的指针、引用型的返回值;
- 如果从名称、参数及返回值三个方面检查之后,派生类的函数满足上述条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数。
- 派生类中的虚函数还会隐藏基类中同名函数的所有其它重载形式。
- 一般习惯于在派生类的函数中也使用
virtual
关键字,以增加程序的可读性。
虚析构函数
为什么需要虚析构函数?
- 可能通过基类指针删除派生类对象;
- 如果你打算允许其他人通过基类指针调用对象的析构函数(通过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;
};
Derived::Derived(){
p = new int(0);
}
Derived::~Derived(){
cout << "Derived destructor" << endl;
delete p;
}
void fun(Base* b){
delete b; // 静态绑定,只会调用~Base()
}
int main(){
Base *b = new Derived();
fun(b);
return 0;
}
运行结果:
Base destructor
#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 destructor
Base destructor
虚表与动态绑定
-
虚表
-
- 每个多态类有一个虚表(virtual table)
- 虚表中有当前类的各个虚函数的入口地址
- 每个对象有一个指向当前类的虚表的指针(虚指针vptr)
-
动态绑定的实现
-
- 构造函数中为对象的虚指针赋值
- 通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址
- 通过该入口地址调用虚函数
虚表示意图
无论是基类的对象,还是派生类的对象,只要是声明了虚函数,就一定会有虚指针包含在对象内部(可以理解为隐形成员变量),虚指针会指向虚表,需表里面有虚函数的入口地址
多态的关键在于动态绑定,也就是编译阶段不直接绑定指针调用的成员函数,因为无法确定指针具体是那个最近类的对象,而是在运行阶段根据实际对象传递给指针的地址或传递给引用的实际内容来确定的。

纯虚函数
-
纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:
virtual 函数类型 函数名(参数表) = 0;
-
带有纯虚函数的类称为抽象类
抽象类
-
带有纯虚函数的类称为抽象类:
class 类名 { virtual 类型 函数名(参数表)=0; //其他成员…… };
抽象类作用
- 抽象类为抽象和设计的目的而声明
- 将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。
- 对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。
注意
- 抽象类只能作为基类来使用。
- 不能定义抽象类的对象。
抽象类举例
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void display() const = 0; //纯虚函数
};
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;
}
override与final
- 多态行为的基础:基类声明虚函数,继承类声明一个函数覆盖该虚函数
- 覆盖要求: 函数签名(signatture)完全一致
下列程序就仅仅因为疏忽漏写了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,不允许被覆盖 };