《C++面向对象程序设计(第4版)》学习笔记-8

在这里插入图片描述

此份笔记建议在完整阅读郑莉老师、董渊老师、何江舟老师所编写的《C++语言程序设计(第4版)》后食用,风味更佳!
最后,由于本人水平有限,笔记中仍存在错误但还没有被检查出来的地方,欢迎大家批评与指正。


第8章 多态性

8.1 多态性概述

多态是指同样的消息被不同类型的对象接收时导致不同的行为。

所谓消息是指对类的成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数。

8.1.1 多态的类型

重载多态、强制多态、包含多态和参数多态

  • 重载多态:普通函数及类的成员函数的重载都属于重载多态
  • 强制多态:强制多态是指将一个变元的类型加以变化,以符合一个函数或者操作的要求,前面所讲的加法运算符在进行浮点数与整型数相加时,首先进行类型强制转换,把整型数变为浮点数再相加的情况,就是强制多态的实例。
  • 包含多态:包含多态是类族中定义于不同类中的同名成员函数的多态行为,主要是通过虚函数来实现。
  • 参数多态:参数多态与类模板(将在第 9 章中介绍)相关联,在使用时必须赋予实际的类型才可以实例化。

8.1.2 多态的实现

1.多态从实现的角度将可以划分成两类:
  • 编译时的多态:在编译的过程中确定了同名操作的具体操作对象
  • 运行时的多态:在程序运行过程中才动态地确定操作所针对的具体对象
2.这种确定操作的具体对象的过程就是绑定

(1)绑定是指计算机程序自身彼此关联的过程,也就是把一个标识符名和一个存储地址联系在一起的过程;用面向对象的术语讲,就是把一条消息和一个对象的方法相结合的过程。

(2)按照绑定进行的阶段的不同,可以分为两种不同的绑定方法:

  • 静态绑定:绑定工作在编译连接阶段完成的情况
    • 通过静态绑定解决,比如重载、强制和参数多态
  • 动态绑定:绑定工作在程序运行阶段完成的情况
    • 包含多态操作对象的确定就是通过动态绑定完成的。

8.2 运算符重载

运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时导致不同的行为。

运算符重载的实质就是函数重载。

这个过程是在编译过程中完成的。

8.2.1 运算符重载的规则

1.运算符重载的规则

(1)C++ 中的运算符除了少数几个之外,全部可以重载,而且只能重载C++中已经有的运算符。

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

(3)运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。一般来讲.重载的功能应当与原有功能相类似,不能改变原运算符的操作对象个数,同时至少要有一个操作对象是自定义类型。

2.不能重载的运算符
  • 类属关系运算符“ . ”
  • 成员指针运算符“ .* ”
  • 作用域分辨符“ :: ”
  • 三目运算符“ ? : ”
3.运算符的重载形式

(1)重载为类的非静态成员函数

返回类型 operator 运算符(参数表)
{
    函数体
}

(2)重载为非成员函数

返回类型 operator 运算符(参数表)
{
    函数体
}
  • 返回类型指定了重载运算符的返回值类型,也就是运算结果类型
  • operator 是定义运算符重载函数的关键字
  • 运算符即是要重载的运算符名称
  • 形参表中给出重载运算符所需要的参数和类型。
4.运算符的两种重载形式的函数的差别
  • 当运算符重载为类的成员函数时,函数的参数个数比原来的操作数个数要少一个(后置"++","–"除外);当重载为非成员函数时,参数个数与原操作数个数相同。
  • 这是因为,重载为类的成员函数时,第一个操作数会被作为函数调用的目的对象,因此无须出现在参数表中,函数体中可以直接访问第一个操作数的成员;而重载为非成员函数时,运算符的所有操作数必须显式通过参数传递。

8.2.2 运算符重载为成员函数

运算符重载实质上就是函数重载,重载为成员函数,它就可以自由地访问本类的数据成员。

实际使用时,总是通过该类的某个对象来访问重载的运算符。

1.重载双目运算符与单目运算符在函数上的区别

(1)单目运算符

  如果是单目运算符,操作数由对象的 this 指针给出,就不再需要任何参数。

(2)双目运算符

  如果是双目运算符, 左操作数是对象本身的数据,由 this 指针指出,右操作数则需要通过运算符重载函数的参数表来传递。

2.具体分析

(1)双目运算符B,重载为成员函数,实现 oprd1 B oprd2。

  • oprd1 是A类的对象
  • 把 B 重载为 A 类的成员函数,该函数只有一个形参,形参的类型是 oprd2 所属的类型。
  • 经过重载之后,表达式 oprd1 B oprd2 相当于函数调用 oprd1.operator B(oprd2) 。

(2)前置单目运算符U,重载为成员函数,实现 U oprd

  • oprd 是A类的对象
  • U 应当重载为A类的成员函数
  • 经过重载之后,表达式 U oprd 相当于调用 oprd.operator U( )

(3)后置单目运算符“ ++ ”、“ – ”,重载为成员函数,实现 oprd ++ 或 oprd –

  • oprd 是A类的对象
  • 重载为A类的成员函数,这时函数需要带一个整型(int)形参
  • 经过重载后,表达式 oprd ++ 或 oprd – 相当于调用函数 oprd.operator ++( 0 ) 和 oprd.operator --( 0 )
3.实例

  题目1:定义一个复数类Complex,重载运算符“+”、“-”、“*”、“~”、“==”,使之能用于复数的加、减、乘、求反以及比较的等运算。要求分别重载运算符函数为Complex类的成员函数。

#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;
	Complex operator* (const Complex &c2) const;
	Complex operator~ () const;
	bool operator== (const Complex &c2) const;
	void display() const;

private:
	double real;
	double imag;
};

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();

	c3 = c1 * c2;
	cout << "c3 = c1 * c2 =";   //复数相乘
	c3.display();

	c3 = ~c1;
	cout << "c3 = ~c1 =";   //复数求共轭
	c3.display();

	cout << "c1 == c2 ?" << "  " << (c1 == c2);

	while (1);
	return 0;
}

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);
}

Complex Complex::operator* (const Complex &c2) const {
	return Complex(real*c2.real-imag*c2.imag,real*c2.imag+c2.real*imag);
}

Complex Complex::operator~ () const {
	return Complex(real, -imag);
}

bool Complex::operator== (const Complex &c2) const {
	if (real == c2.real && imag == c2.imag)
	{
		return true;
	}
	else
	{
		return false;
	}
}

void Complex::display() const
{
	cout << "(" << real << "," << imag << ")" << endl;
}
//输出结果
c1=(5,4)
c2=(2,10)
c3 = c1 + c2 =(7,14)
c3 = c1 - c2 =(3,-6)
c3 = c1 * c2 =(-30,58)
c3 = ~c1 =(5,-4)
c1 == c2 ?  0

  题目2:声明Point类,有坐标x和y两个成员变量;对Point类重载“++”、“–”,自增、自减(前置、后置)运算符,实现对坐标值的改变。

#include <iostream>
using namespace std;

class Point
{
public:
	Point(int xx=0, int yy=0) :x(xx), y(yy) {}
	Point & operator++();       //前置++
	Point operator++(int);      //后置++
	Point & operator--();       //前置--
	Point operator--(int);      //后置--
	void show();

private:
	int x;
	int y;
};

int main()
{
	Point p1(3, 5), p2(1, 1);
	p2.show();
	cout << endl;

	p2 = ++p1;
	p2.show();
	p1.show();
	cout << endl;

	p2 = p1++;
	p2.show();
	p1.show();
	cout << endl;

	p2 = --p1;
	p2.show();
	p1.show();
	cout << endl;

	p2 = p1--;
	p2.show();
	p1.show();

	while (1);
	return 0;

}

Point& Point::operator++()
{
	x++;
	y++;
	return *this;
}

Point Point::operator++(int)
{
	Point old = *this;
	x++;
	y++;
	return Point(old.x, old.y);
}

Point& Point::operator--()
{
	x--;
	y--;
	return *this;
}

Point Point::operator--(int) 
{
	Point old = *this;
	x--;
	y--;
	return Point(old.x, old.y);
}

void Point::show()
{
	cout << "(" << x << "," << y << ")" << endl;
}
//输出结果
(1,1)

(4,6)
(4,6)

(4,6)
(5,7)

(4,6)
(4,6)

(4,6)
(3,5)

8.2.3 运算符重载为非成员函数

1.基本概述
  • 运算符也可以重载为非成员函数。这时,运算所需要的操作数都需要通过函数的形参表来传递,在形参表中形参从左到右的顺序就是运算符操作数的顺序。
  • 如果需要访问运算符参数对象的私有成员,可以将该函数声明为类的友元函数。
2.讨论

(1)双目运算符

  • 如果要实现 oprd1 B oprd2 , 其中 oprd1 和 oprd 2 中只要有一个具有自定义类型,就可以将 B 重载为非成员函数,函数的形参为 oprd1 和 oprd2 。
  • 经过重载之后 ,表达式 oprd1 B oprd2 就相当于函数调用 operator B(oprd1, oprd2) 。

(2)前置单目运算符

  • 如果要实现表达式 U oprd,其中 oprd 具有自定义类型,就可以将 U 重载为非成员函数,函数的形参为 oprd
  • 经过重载之后,表达式 U oprd 相当千函数调用 operator U ( oprd ) 。

(3)后置单目运算符

  • 如果要实现表达式 oprd + + 或 oprd – ,其中 oprd 为自定义类型,那么运算符就可以重载为非成员函数。这时函数的形参有两个,一个是oprd, 另一个是 int 类型形参。第二个参数是用于与前置运算符函数相区别的。
  • 重载之后,表达式 oprd + + 和 oprd – 就 相 当于函数调用 operator ++ (oprd, 0 ) 和operator – (oprd, 0) 。
3.实例

  题目1:定义一个复数类Complex,重载运算符“+”、“-”、“*”、“~”、“==”,使之能用于复数的加、减、乘、求反以及比较的等运算。要求分别重载运算符函数为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 Complex operator* (const Complex &c1, const Complex &c2);
	friend Complex operator~ (const Complex &c1);
	friend bool operator== (const Complex &c1, const Complex &c2);
	void display() const;

private:
	double real;
	double imag;
};

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();

	c3 = c1 * c2;
	cout << "c3 = c1 * c2 =";
	c3.display();

	c3 = ~c1;
	cout << "c3 = ~c1 =";
	c3.display();

	cout << "c1 == c2 ?" << "  " << (c1 == c2);

	while (1);
	return 0;
}

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);
}

Complex operator* (const Complex &c1, const Complex &c2) {
	return Complex(c1.real*c2.real - c1.imag * c2.imag, c1.real*c2.imag + c2.real*c1.imag);
}

Complex operator~ (const Complex &c1) {
	return Complex(c1.real, -c1.imag);
}

bool operator == (const Complex &c1, const Complex &c2) {
	if (c1.real == c2.real && c1.imag == c2.imag)
	{
		return true;
	}
	else
	{
		return false;
	}
}

void Complex::display() const
{
	cout << "(" << real << "," << imag << ")" << endl;
}
//输出结果
c1=(5,4)
c2=(2,10)
c3 = c1 + c2 =(7,14)
c3 = c1 - c2 =(3,-6)
c3 = c1 * c2 =(-30,58)
c3 = ~c1 =(5,-4)
c1 == c2 ?  0

  题目2:声明Point类,有坐标x和y两个成员变量;对Point类重载“++”、“–”,自增、自减(前置、后置)运算符,实现对坐标值的改变。

#include <iostream>
using namespace std;

class Point
{
public:
	Point(int xx = 0, int yy = 0) :x(xx), y(yy) {}
	friend Point & operator++(Point &p);       //前置++
	friend Point operator++(Point &p, int);      //后置++
	friend Point & operator--(Point &p);       //前置--
	friend Point operator--(Point &p, int);      //后置--
	void show();

private:
	int x;
	int y;
};

int main()
{
	Point p1(3, 5), p2(1, 1);
	p2.show();
	cout << endl;
	
    p2 = ++p1;
	p2.show();
	p1.show();
	cout << endl;
	
    p2 = p1++;
	p2.show();
	p1.show();
	cout << endl;
	
    p2 = --p1;
	p2.show();
	p1.show();
	cout << endl;
	
    p2 = p1--;
	p2.show();
	p1.show();

	while (1);
	return 0;
}

Point & operator++(Point &p)
{
	p.x++;
	p.y++;
	return p;
}

Point operator++(Point &p, int)
{
	Point old = p;
	p.x++;
	p.y++;
	return Point(old.x, old.y);
}

Point & operator--(Point &p)
{
	p.x--;
	p.y--;
	return p;
}

Point operator--(Point &p, int)
{
	Point old = p;
	p.x--;
	p.y--;
	return Point(old.x, old.y);
}

void Point::show()
{
	cout << "(" << x << "," << y << ")" << endl;
}
//输出结果
(1,1)

(4,6)
(4,6)

(4,6)
(5,7)

(4,6)
(4,6)

(4,6)
(3,5)

8.3 虚函数

虚函数是动态绑定的基础。虚函数必须是非静态的成员函数。虚函数经过派牛之后,在类族中就可以实现运行过程中的多态。

1.抛出问题

  根据赋值兼容规则,可以使用派生类的对象代替基类对象。如果用基类类型的指针指向派生类对象,就可以通过这个指针来访问该对象,问题是访问到的只是从基类继承来的同名成员。

2.解决这一问题的办法是:

  如果需要通过基类的指针指向派生类的对象,并访问某个与基类同名的成员,那么首先在基类中将这个同名函数说明为虚函数。这样,通过基类类型的指针,就可以使属于不同派生类的不同对象产牛不同的行为,从而实现运行过程的多态。

8.3.1 一般虚函数成员

1.一般虚函数成员的声明语法
virtual 函数类型 函数名(参数表)
  • 虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
2.注意

  虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定.而对内联函数的处理是静态的,所以虚函数一般不能以内联函数处理。但将虚函数声明为内联函 数也不会引起错误。

3.实例
#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:
    void display() const;
};
void Base2::display() const {
    cout << "Base2::display()" << endl;
}

class Derived:public Base2{
public:
    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;
}
//输出结果
Base1::display()
Base2::display()
Derived::display()
  • 这里,派生类的虚函数覆盖了基类的虚函数。不仅如此,派生类中虚函数还会隐藏基类中同名函数的所有其他重载形式。
  • 不过,用指向派生类对象的指针仍然可以调用基类中被派生类覆盖的成员函数,方法是使用 “ :: ”进行限定。
    • ptr->Base1::display( )
  • 派生类覆 盖基类的成员函数时,既可以使用 virtual 关键字 ,也可以不使用 ,二者没有差别(系统会自动判断)。 很多人习惯于在派生类的函数中也使用 virtual 关键字,因为这样可以清楚地提示这是一个虚函数。
  • 通过基类类型的指针就可以访问到正在指向的对象的成员,这样,能够对同一类族中的对象进行统一的处理,抽象程度更高,程序更简洁、更高效。
4.系统判断派生类的一个函数成员是不是虚函数(需要在派生类每个于基类虚函数同名的函数前都加上virtual?)
  • 该函数是否与基类的虚函数有相同的名称。
  • 该函数是否与基类的虚函数有相同的参数个数及相同的对应参数类型。
  • 该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容规则的指针、引用型的返回值。

  如果从名称、参数及返回值 3 个方面检查之后,派生类的函数满足了上述条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数。不仅如此,派生类中的虚函数还会隐藏基类中同名函数的所有其他重载形式。

  派生类覆 盖基类的成员函数时,既可以使用 virtual 关键字 ,也可以不使用 ,二者没有差别(系统会自动判断)。 很多人习惯于在派生类的函数中也使用 virtual 关键字,因为这样可以清楚地提示这是一个虚函数。

5.其他
  • 当基类构造函数调用虚函数时,不会调用派生类的虚函数。
    • 例如:派生类的构造函数中,首先调用的基类的构造函数,这时基类的构造函数调用了虚函数,那么调用的这个虚函数是基类的,而不会是派生类对象的。这是因为当基类被构造时,对象还不是一个派生类的对象。
  • 同样,当基类析构函数调用虚函数时,不会调用派生类的虚函数。因为当基类被析构时 ,对象已经不再是一个派生类对象了。
6.只有虚函数是动态绑定的
  • 如果派生类需要修改基类的行为(即重写与基类函数同名的函数),就应该在基类中将相应的函数声明为虚函数。
  • 而基类中声明的非虚函数,通常代表那些不希望被派生类改变的功能,也是不能实现多态的。
  • 一般不要重写继承而来的 非虚函数(虽然语法对此没有强行限制),因为那会导致通过基类指针和派生类的指针或对象调用同名函数时,产生不同的结果,从而引起混乱。
7.只有通过基类的指针或引用调用虚函数时,才会发生动态绑定。
  • 基类的指针可以指向派生类的对象,基类的引用可以作为派生类对象的别名,但基类的对象却不能表示派生类的对象。
Derived d;
Base *ptr = &d;   //基类指针 ptr 可以指向派生类对象
Base &ref = d;    //基类引用 ref 可以作为派生类对象的别名
Base b = d;       //调用 Base1 的复制构造函数用 d 构造 b,b 的类型是 Base1而非Derived
  • 这里,Base b = d 会用 Derived 类型的对象 d 为 Base 类型的对象 b 初始化 ,初始化时使用的是 Base 的复制构造函数。由于复制构造函数接收的是 Base 类型的常引用,Derive d 类型的 d 符合类型兼容性规则,可以 作为参数传递给它。由于执行的是 Base 的复制构造函数,只有 Base 类型的成员会被复制,Derive d 类中新增的数据成员既不会被复制,也没有空间去存储 ,因此生成的对象是基类 Base 的对象。
  • 这种用派生类对象复制构造基类对象的行为称做对象切片
  • 这时,如果用 b 调用 Base 类的虚函数,调用的目的对象是对象切片后得到的 Base 对象,与 Derived 类型的 d 对象全无关系,对象的类型很明确,因此无须动态绑定。

8.3.2 虚析构函数

在 C++ 中 ,不能声明虚构造函数,但是可以声明虚析构函数。

1.虚析构函数的声明语法
virtual ~类名();
2.讨论
  • 如果一个类的析构函数是虚函数,那么由它派生而来的所有子类的析构函数也是虚函数。
  • 析构函数设置为虚函数之后,在使用指针引用时可以动态绑定,实现运行时的多态,保证使用基类类型的指针就能够调用适当的析构函数针对不同的对象进行清理工作。
3.实例
#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;
}

int main()
{
    Base *b = new Derived();
    fun(b);
    return 0;
}
//输出结果
Base destructor
  • 这说明,通过基类指针删除派生类对象时调用的是基类的析构函数,派生类的析构函数没有被执行,因此派生类对象中动态分配的内存空间没有得到释放,造成了内存泄漏。也就是说派生类对象成员 p 所指向的内存空间,在对象消失后既不能被本程序继续使用也没有被释放。对千内存需求量较大、长期连续运行的程序来说,如果持续发生这样的错误是很危险的,最终将导致因内存不足而引起程序终止。
  • 避免上述错误的有效方法就是将析构函数声明为虚函数:
class Base{
public:
    virtual ~Base();
};
Base::~Base(){
    cout << "Base destructor" << endl;
}
//输出结果
Derived destructor
Base destructor
  • 使用虚函数技术,使派生类的析构函数覆盖了基类的虚构函数,也就是通过一个基类的指针,可以调用派生类的析构函数。而执行派生类的析构函数,会自动调用基类的析构函数。完美!

8.4 纯虚函数与抽象类

抽象类是带有纯虚函数的类。

8.4.1 纯虚函数

1.什么是纯虚函数
  • 纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要给出各自的定义。
2.纯虚函数的声明格式
virtual 函数类型 函数名(参数表)=0
  • 声明为纯虚函数之后,基类中就可以不再给出函数的实现部分。
  • 纯虚函数的函数体由派生类给出。
  • 基类中仍然允许对纯虚函数给出实现,但即使给出实现,也必须由派生类覆盖,否则无法实例化。
    • 在基类中对纯虚函数定义的函数体的调用,必须通过“基类名::函数名(参数表)”的形式。
  • 如果将析构函数声明为纯虚函数,必须给出它的实现,因为派生类的析构函数体执行完后需要调用基类的纯虚函数。

8.4.2 抽象类

1.抽象类是带有纯虚函数的类。
  • 抽象类派生出新的类之后,如果派生类给出所有纯虚函数的函数实现,这个派生类就可以定义自己的对象,因而不再是抽象类;
  • 反之,如果派生类没有给出全部纯虚函数的实现,这时的派生类仍然是一个抽象类。
2.抽象类不能实例化
  • 即不能定义一个抽象类的对象,但是可以定义一个抽象类的指针和引用。
  • 通过指针或引用,就可以指向并访问派生类的对象,进而访问派生类的成员,这种访问是具有多态特征的。

猜你喜欢

转载自blog.csdn.net/Jason3633/article/details/91965439