C++学习笔记:08 多态性

课程《C++语言程序设计进阶》清华大学 郑莉老师)

基本概念

多态性

具体的讲,在面向对象程序设计中,指同样的方法被不同对象执行时会有不同的执行效果。

多态的实现

  1. 绑定机制

    绑定是将一个标识符名和一个存储地址联系在一起的过程

  2. 静态多态性:编译时的多态通过静态绑定实现,例如 函数的重载

    绑定工作在编译连接阶段完成

  3. 动态多态性:运行时的多态通过动态绑定实现,例如 虚函数

    绑定工作在程序运行阶段完成

运算符重载

函数重载就体现了静态多态性。运算符也是同样的符号经过重载可以作用于不同的数据类型,可以为类重载不同的操作符,使同一个运算符作用于不同类型的数据时导致不同的行为。例如为复数类重载+,-,*,/等基本操作符,实现复数基本运算法则。

  • C++ 几乎可以重载全部的运算符,而且只能够重载C++中已经有的。不能重载的运算符:“.”、“.*”、“::”、“?:”

  • 重载之后运算符的优先级和结合性都不会改变

  • 重载为类的非静态成员函数

  • 重载为非成员函数

重载为成员函数

函数类型 operator 运算符(形参)
{
    函数定义
}

双目运算符重载规则:

运算所需变量为两个的运算符叫做双目运算符,例如:+,-,*,等等

//B为双目运算符,obj1,obj2为类对象
obj1 B obj2
/*等效于*/
obj1.operator B(obj2)

例如:

#include <iostream>
using namespace std;
class Complex {
private:
	double real, image;
public:
	Complex(double r = 0., double i = 0.) :real(r), image(i) {}
	Complex operator +(const Complex& c2) const;	//重载+
	Complex operator -(const Complex& c2) const;	//重载-
	friend ostream& operator <<(ostream& os, const Complex& c2);
};
Complex Complex::operator +(const Complex& c2) const {
	return Complex(this->real + c2.real, this->image + c2.image);
}
Complex Complex::operator -(const Complex& c2) const {
	return Complex(this->real - c2.real, this->image - c2.image);
}
ostream& operator <<(ostream& os, const Complex& c2) {
	os << "(" << c2.real << "," << c2.image << ")" << endl;
	return os;
}

int main() {
	Complex c1(1, 1), c2(2, 3);
	Complex c3 = c1 + c2;
	cout << c1 << c2 << c3;
	return 0;
}

单目运算符重载

运算所需变量为一个的运算符叫做单目运算符,例如:++,--,等等

前置单目运算符

//U为单目运算符,obj1为类对象
U obj1
/*等效于*/
obj1.operator U()

后单目运算符

与前置相比,为了加以区分,后置单目运算符会具有一个int类型形参

//U为单目运算符,obj1为类对象
U obj1
/*等效于*/
obj1.operator U(0)

例如:

#include <iostream>
using namespace std;
class Count {
private:
    int num;
public:
    Count(int _num) :num(_num) {}
    Count& operator ++();//前置++
    Count operator ++(int);//后置++
    friend ostream& operator << (ostream& os, const Count c);
};
Count& Count::operator ++ () {
    num++;
    return *this;
}
Count Count::operator ++ (int) {
    Count old = *this;
    ++(*this);  //调用前置“++”运算符
    return old; //返回++之前的值
}
ostream& operator<<(ostream& os, const Count c){
    os << "Current Num = " << c.num << endl;
    return os;
}
int main() {
    Count myCount(10);
    cout << "myCount  :    ";
    cout << myCount;
    cout << "myCount++:    ";
    cout << myCount++;
    cout << "++myCount:    ";
    cout << ++myCount;
    return 0;
}
/*
myCount  :    Current Num = 10
myCount++:    Current Num = 10
++myCount:    Current Num = 12
*/

运算符重载为非成员函数

双目运算符的左操作数不是对象,比如,对于以上复数类,需要实现double+Complex类型运算的功能,则需要在类外实现运算符重载函数。

例如:在complex类中,将+与<<重载为类外操作符。

#include <iostream>
using namespace std;
class Complex {
private:
	double real, image;
public:
	Complex(double r = 0., double i = 0.) :real(r), image(i) {}
	friend Complex operator + (double c1, const Complex& c2);	//重载类外+
	friend ostream& operator <<(ostream& os, const Complex& c2);
};
Complex operator+(double c1, const Complex& c2) {
	return Complex(c1 + c2.real, c2.image);
}
ostream& operator <<(ostream& os, const Complex& c2) {
	os << "(" << c2.real << "," << c2.image << ")" << endl;
	return os;
}
int main() {
	double c1 = 10;
	Complex c2(1, 1);
	Complex c3 = c1 + c2;
	cout << "double " << c1 << endl;
	cout << "complex " << c2 << "double +complex " << c3;
	return 0;
}
/*
double 10
complex (1,1)
double +complex (11,1)
*/

虚函数

通过virtual关键词,让程序能在运行时根据指针指向的实际对象,找到该对象的函数。使用虚函数实现动态绑定,也就是C++的动态多态性。

虚函数特点

  • 用virtual关键字说明的函数

  • 虚函数是实现运行时多态性基础

  • C++中的虚函数是动态绑定的函数

  • 虚函数必须是非静态的成员函数,虚函数经过派生之后,就可以实现运行过程中的多态。

  • 一般成员函数可以是虚函数

  • 构造函数不能是虚函数

  • 析构函数可以是虚函数

例如:例子中的fun()函数,传递不同类型的对象,调用特定对象的display()函数

#include <iostream>
using namespace std;

class Base1 {
public:
    virtual void display() const { cout << "Base1::display()" << endl; };  //虚函数
};
class Base2 :public Base1 {
public:
    virtual void display() const { cout << "Base2::display()" << endl; }
};
class Derived : public Base2 {
public:
    virtual void 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);
    derived.Base1::display();
    derived.Base2::display();
    derived.display();
    return 0;
}
/*
Base1::display()
Base2::display()
Derived::display()
Base1::display()
Base2::display()
Derived::display()
*/

虚函数

虚函数的声明

virtual 函数类型 函数名(形参表);
  • 虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
  • 在派生类中可以对基类中的成员函数进行覆盖
  • 虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的。

virtual 关键字

  • 派生类可以不显式地用virtual声明虚函数,这时系统就会用以下规则来判断派生类的一个函数成员是不是虚函数:
  • 该函数是否与基类的虚函数有相同的名称、参数个数及对应参数类型
  • 该函数是否与基类的虚函数有相同的返回值或者满足类型兼容规则的指针、引用型的返回值;
  • 如果从名称、参数及返回值三个方面检查之后,派生类的函数满足上述条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数
  • 派生类中的虚函数还会隐藏基类中同名函数的所有其它重载形式。
  • 一般习惯于在派生类的函数中也使用virtual关键字,以增加程序的可读性。

虚析构函数

为什么需要虚析构函数?

  • 可能通过基类指针删除派生类对象
  • 如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),就需要让基类的析构函数成为虚函数,否则执行delete的结果是不确定的。

例子:虚析构函数

#include <iostream>
using namespace std;
class Base {
public:
    virtual ~Base() { cout << "Base destructor" << endl; }
};

class Derived : public Base {
public:
    Derived() { p = new int(0); }
    virtual ~Derived() { cout << "Derived destructor" << endl; delete p; }
private:
    int* p;
};

void fun(Base* b) {    delete b;}

int main() {
    Base* base = new Base();
    Base* derived = new Derived();
    fun(base);
    fun(derived);
    return 0;
}
/*
Base destructor
Derived destructor
Base destructo
*/

虚函数的实现

虚表与动态绑定

虚表

  1. 每个多态类有一个虚表(virtual table)

    虚表中有当前类的各个虚函数的入口地址

    每个对象有一个指向当前类的虚表的指针(虚指针vptr)

  2. 动态绑定的实现

    构造函数中为对象的虚指针赋值

    通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址

    通过该入口地址调用虚函数纯虚类,定义统一的函数接口

    image-20210914103019879

抽象类

纯虚函数

纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:

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 { cout << "Base2::display()" << endl; } //覆盖基类的虚函数
};
class Derived : public Base2 {
public:
    virtual void display() const { cout << "Derived::display()" << endl; } //覆盖基类的虚函数
};
void fun(Base1* ptr) {  //定义引用的通用接口
    ptr->display();
}
int main() {
    Base2 base2;
    Derived derived;
    fun(&base2);
    fun(&derived);
    return 0;
}
/*
Base2::display()
Derived::display()
*/

override 与 final关键字

override

  • 多态行为的基础:基类声明虚函数,继承类声明一个函数覆盖该虚函数

  • 覆盖要求: 函数签名(signatture)完全一致

  • 函数签名包括:函数名 参数列表 const(通常可能会忽略const,导致虚函数没有被覆盖)

下列程序就仅仅因为疏忽漏写了const,导致多态行为没有如期进行

image-20210914103856598

final

C++11提供的final,用来避免类被继承,或是基类的函数被改写

//类限定final
struct Base1 final { };
struct Derived1 : Base1 { }; // 编译错误:Base1为final,不允许被继承
//类成员函数限定final
struct Base2 {
virtual void f() final;
};
struct Derived2 : Base2 {
void f(); // 编译错误:Base2::f 为final,不允许被覆盖
};
posted @ 2021-09-14 10:46  陈橙橙  阅读(186)  评论(0编辑  收藏  举报