jcLee95 的个人博客
已入驻阿里云博客
邮箱 :291148484@163.com
本文地址:
- https://developer.aliyun.com/article/
- https://blog.csdn.net/qq_28550263/article/details/125348483
目 录
- 4.1 虚函数的声明方式
- 4.2 虚函数属于对象
- 4.3 虚函数在子类可不用
virtual
声明 - 4.4 虚函数实现动态解析的调用条件
- 4.5 什么函数可以是虚函数
- 4.6 析构虚函数
- 4.7 虚表 与 动态绑定
1. 多态的概念
1.1 什么是多态
在编程中,多态的体现就是一个操作接口具有表现多种不同形态的能力,对不同的输入有不同的处理方式。例如, +
运算符,既可以适用于整数的相加,也可以适用于浮点数的相加,这就是多态性的体现。
1.2 C++ 多态实现原理
C++中的多态是是通过绑定 来实现的,其中绑定指将一个标识符名称与一段函数代码结合起来。依据绑定实现的时期,可以分为 编译时的绑定 和 运行时的绑定 。
编译时的绑定 ,故名思意,在编译器对代码进行编译的时候就已经完成了绑定。而 运行时的绑定 需要等到程序运行的时候才将标识符和相应的行数代码结合起来。
2. 函数的重载
在C++中 函数的重载 是一种 静态多态性,是通过 编译时绑定 来完成的。
3. 运算符的重载
在C++中 运算符的重载 是一种 动态多态性,是通过 运行时绑定 来完成的。
3.1 概述
运算符重载让我们能够使用系统里面预先定义好的运算符进行相应运算。例如 定义一个 复数类,这是C++原生类型中不包含的类型。那么我们若想要通过C++中的运算符来完成复数的运算,则需要重载相应的运算符来实现。
C++ 几乎可以重载全部C++中已经有的运算符,但不能重载以下运算符:
运算符 | 描述 |
“. ” |
成员访问运算符; |
“.* ”、->* |
成员指针访问运算符; |
“:: ” |
域运算符; |
“sizeof ” |
长度运算符; |
“?: ” |
条件运算符。 |
3.2 双目运算符 重载
重载运算符是通过定义类的成员函数来实现的,重载函数名是由关键字 operator
和其后要重载的运算符符号构成的。其格式示意如下:
函数类型 operator 运算符(形参列表){ // 函数体 }
例如,定义一个复数类:
class Complex { private: double r; // 实部 double i; // 虚部 public: // 构造一个复数对象:指定实部和虚部的值作为该复数对象的实部和虚部 Complex(double r = 0.0, double i = 0.0) : r(r), i(i) { } Complex operator+(const Complex& c2) const { // 将一个临时无名复数对象用作返回值 return Complex(r + c2.r, i + c2.i); } Complex operator-(const Complex& c2) const { // 同理 return Complex(r - c2.r, i - c2.i); } void print() const { cout << r << " + " << i << "j" << endl; } };
与其他函数一样,重载运算符有一个返回类型和一个参数列表。除后置++、–外,其中形参的个数为该运算符的操作数的个数减1。
3.3 单目运算符 重载
单目运算符是只对一个操作数进行操作的运算符。如位运算符、自增/自减运算符
3.3.1 前置单目运算符 的重载
如果要重载 U 为类 成员函数,使之能够实现表达式 U oprd,其中 oprd 为A类对象,则 U 应被重载为 A 类的成员函数,无形参。
经重载后,表达式 U oprd
相当于 oprd.operator U()
例如:
3.3.2 后置单目运算符 ++
和--
的重载
如果要重载 ++或–为类成员函数,使之能够实现表达式 oprd++
或 oprd--
,其中 oprd 为A类对象,则 ++或-- 应被重载为 A 类的成员函数,且具有一个 int 类型形参。
经重载后,表达式 oprd++
相当于 oprd.operator ++(0)
例如:
3.4 关系运算符重载
关系运算符包括 <
、 >
、 <=
、 >=
、 ==
。现在我们通过重载关系运算符来实现两个复数类实例的关系比较。在此我们定义:
- 两个复数的实部和虚部相等,则认为两个复数相等(==为真);
- 若复数a的模比复数b的模大,则表示方式 a>b 返回真,反之返回假;
- 若复数a的模等于复数b的模 或 复数 a 的模大于复数 b 的模,则表示方式 a >= b 返回真。类似地,同理定义 a <= b。
实现和调用测试如下:
#include <iostream> #include <cmath> using namespace std; class Complex { private: double r; // 实部 double i; // 虚部 public: Complex(double r = 0.0, double i = 0.0) : r(r), i(i) { } Complex operator+(const Complex& c2) const { return Complex(r + c2.r, i + c2.i); } Complex operator-(const Complex& c2) const { return Complex(r - c2.r, i - c2.i); } // 计算复数的模长 double length() { return pow((pow(r, 2)+pow(i,2)),0.5); } // 重载关系运算符 " == " bool operator == (const Complex cplx) { if(r == cplx.r && i == cplx.i) { return true; }else{ return false; } } // 重载关系运算符 " > " bool operator > (Complex cplx){ if(length() > cplx.length()){ return true; } return false; } // 重载关系运算符 " < " bool operator < (Complex cplx){ if (length() < cplx.length()) { return true; } return false; } // 重载关系运算符 " >= " bool operator >= (Complex cplx) { double length = cplx.length(); if (this->length() > length || this->length() == length) { return true; } return false; } // 重载关系运算符 " >= " bool operator <= (Complex cplx) { double length = cplx.length(); if (this->length() < length || this->length() == length) { return true; } return false; } void print() const { cout << r << " + " << i << "j" << endl; } }; int main(){ Complex c1(3, 4), c2(5, 3), c3(4, 3); cout << "c1.length() = " << c1.length() << "\n"; cout << "c2.length() = " << c2.length() << "\n"; cout << "c3.length() = " << c3.length() << "\n"; if (c1 > c2) { cout << "c1 > c2\n"; } else if(c1 < c2){ cout << "c1 < c2\n"; } else if (c1 == c2) { cout << "c1 == c2\n"; } if(c1 >= c2){ cout << "c1 >= c2\n"; } else if (c1 <= c2) { cout << "c1 <= c2\n"; } // 是否相等(虚部实部同时相等) if(c1 == c3){ cout << "c1 == c3\n"; }else{ cout << "c1 != c3\n"; } }
Out[]:
c1.length() = 5 c2.length() = 5.83095 c3.length() = 5 c1 < c2 c1 <= c2 c1 != c3
3.5 输入/输出运算符重载
C++中我们使用 流提取运算符 >>
和 流插入运算符 <<
来 输入和 输出 内置的数据类型。
我们可以通过重载 流这两个运算符 ,以实现将他们用于自定义类型,表示提取和插入操作。
为了不需要创建对象,而直接调用函数,我们可以把运算符重载函数声明为类的友元函数。
例如,为了方便直接在控制台输入输出复数,我们为复数类重载输入输出运算符:
#include <iostream> #include <cmath> using namespace std; class Complex { private: double r; // 实部 double i; // 虚部 public: Complex(double r = 0.0, double i = 0.0) : r(r), i(i) { } Complex operator+(const Complex& c2) const { return Complex(r + c2.r, i + c2.i); } Complex operator-(const Complex& c2) const { return Complex(r - c2.r, i - c2.i); } // 重载运算符 " << " friend ostream& operator<<(ostream& output, const Complex& c){ output << c.r << " + " << c.i << "i"; return output; } // 重载运算符 " >> " friend istream& operator>>(istream& input, Complex& D) { input >> D.r >> D.i; return input; } }; int main(){ Complex c1(3, 4), c2; cout << "c1 = " << c1 << endl; cout << "Please enter the value of c2(real and image) : " << endl; cin >> c2; cout << "c2 = " << c2; }
Out[]:
c1 = 3 + 4i Please enter the value of c2(real and image) :
输入:
6 7
Out[]:
c2 = 6 + 7i
如图所示:
3.6 赋值运算符重载
例如:
3.7 函数调用运算符 ()
的 重载
函数调用运算符 ()
可以被重载用于类的对象,例如:
#include <iostream> using namespace std; class Point { private: int x; int y; public: // 构造函数 Point(int x0, int y0) { x = x0; y = y0; } // 默认构造函数 Point(){ x = 0; y = 0; } void print() { cout << "x = " << x << "; y = " << y << endl; } // 重载运算符函数 Point operator()(int stepx1, int stepy1, int stepx2, int stepy2) { Point M; M.x = x + stepx1 - stepx2; M.y = y + stepy1 - stepy2; return M; } }; int main(){ Point p1(10, 10), p2; cout << "Point 1: "; p1.print(); p2 = p1(20,5,15,3); cout << "Point 2: "; p2.print(); return 0; }
Out[]:
Point 1: x = 10; y = 10 Point 2: x = 15; y = 12
可见,通过重载()
,我们可以创建一个能够传递任意数目参数的运算符函数,本例中相当于传入了两个点的坐标。
3.8 下标运算符 []
的 重载
下标运算符 []
通常用于访问数组元素。最典型的应用场景,当我们自定义一个数据容器类如自定义一个数组类、链表类等等,想通过 下标运算符 [] 访问其中的某个元素时,这时我们重载[]
来实现。
例如,我们实现一个动态数组:
#include <iostream> typedef int Rank; using namespace std; #define INIT_CAPACITY 3 template <typename T> class Vector { protected: Rank _size; int _capacity; T* _elem; // 扩容 void _expand() { if (_size < _capacity)return; if (_capacity < INIT_CAPACITY) { _capacity = INIT_CAPACITY; }; T* oldElem = _elem; _elem = new T[_capacity <<= 1]; for (int i = 0; i < _size; i++) { _elem[i] = oldElem[i]; } delete[] oldElem; } // 缩容 void _shrink() { if (_capacity < INIT_CAPACITY << 1)return; if (_size << 2 * _capacity)return; T* oldElem = _elem = new T[_capacity >>= 1]; for (int i = 0; i < _size; i++) { _elem[i] = oldElem[i]; } delete[] oldElem; } public: // 构造 Vector(int c = INIT_CAPACITY, int s = 0, T v = 0) { _elem = new T[_capacity = c]; for (_size = 0; _size < s; _elem[_size++] = v); } // 析构 ~Vector() { delete[] _elem; } // 重载 下标运算符 [] T& operator[] (Rank r)const { return _elem[r]; } // 插入元素 Rank insert(Rank r, T const& e){ _expand();// 扩容 // 移动元素 for (int i = _size; i > r; i--){ _elem[i] = _elem[i - 1]; } _elem[r] = e; // 放入新元素 _size++; // 容量加1 return r; } // 尾插 Rank append(T const& e){ return insert(_size, e); } }; int main(){ Vector<int> a; a.append(1); a.append(3); a.append(5); a.append(7); a.append(9); cout << "a[0] = " << a[0] << endl; cout << "a[3] = " << a[3] << endl; cout << "a[4] = " << a[4] << endl; }
Out[]:
a[0] = 1 a[3] = 7 a[4] = 9
3.9 类成员访问运算符 ->
的 重载
类成员访问运算符( ->
)被定义用于为一个类赋予 “指针” 行为,
4. 虚函数
4.1 虚函数的声明方式
虚函数 是在基类中使用关键字 virtual
声明的函数,它能够用于实现 动态绑定。其声明格式为:
virtual 函数类型 函数名(形参表);
4.2 虚函数属于对象
在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。虚函数必须是非静态的成员函数,虚函数经过派生之后,就可以实现 运行过程中的多态(运行时多态)。
也就是说 虚函数 是属于某个实例对象,而非属于整个类的,因而需要在运行的时候 通过 指针 去定位到它所实际指向的对象是谁,然后决定调用那个函数体,因此调用的对象不同时,同一个虚函数就实现了运行时的 多态。
4.3 虚函数在子类可不用virtual
声明
在基类中声明为virtual
的函数 在从基类派生的所有类中都是虚函数,不论派生类中是否使用 virtual
进行声明。
4.4 虚函数实现动态解析的调用条件
使用对象调用虚函数总是进行静态解析,只有通过指针或者引用调用的虚函数,才会进行动态解析。
4.5 什么函数可以是虚函数
- 一般的成员函数 可以 是虚函数;
- 析构函数 可以 是虚函数,见 4.6 析构虚函数 部分;
- 构造函数 不可以 是虚函数。
对于一般的成员函数,虚函数只能出现在类定义中的函数原型中声明,而不能在成员函数实现的时候声明;
在派生类中可以对基类成员函数进行重写以覆盖;
虚函数一般不声明为内联函数,因为虚函数的调用需要动态绑定,而对内联函数的处理是静态的。
4.6 析构虚函数
当通过基类指针删除派生类对象时, 如果你打算允许其他人通过基类指针调用对象的析构函数,就需要让基类的析构函数成为虚函数。
如果 基类的析构函数 不是虚函数,通过基类指针删除派生类对象时,执行delete的结果是不确定的。
例如,当基类析构函数不是 虚函数时:
// 基类 class Base { public: //不是虚函数 ~Base(){ cout<< "基类的析构函数" << endl; } }; // 派生类 class Derived: public Base { public: Derived(){ p = new int(0); } //不是虚函数 ~Derived(){ cout << "派生类的析构函数" << endl; delete p; } private: int *p; }; void fun(*Base b){ delete b; // 静态绑定,指挥调用 ~Base() } int main(){ Base *b = new Derived(); fun(b); // 通过基类指针删除派生类对象 return 0; }
Out[]:
基类的析构函数
而当基类析构函数不是 虚函数时:
// 基类 class Base { public: virtual ~Base() { cout<< "基类的析构函数" << endl; } }; // 派生类 class Derived: public Base{ public: Derived(){ p = new int(0); } virtual ~Derived(){ cout << "派生类的析构函数" << endl; } private: int *p; }; int main(){ Base *b = new Derived(); fun(b); return 0; }
Out[]:
派生类的析构函数 基类的析构函数
4.7 虚表 与 动态绑定
4.7.1 虚表的概念
虚函数为什么可以在运行时实现动态绑定呢?实际上时编译器在编译阶段为我们程序中含有虚函数的类都生成了一个虚表(virtual table), 虚表中有 当前类的 各个虚函数 的 入口地址。每个 对象 有一个 指向当前类的虚表 的 指针(虚指针)。
4.7.2 动态绑定的原理
在程序运行时,如果有一个指针指向了一个对象,需要通过这个指针去调用一个函数的时候,先会通过对象的指针先找其 虚表的指针(虚指针),从而找到 虚表,从虚表中,进一步找到指向相应虚函数的指针,最后实现调用相应的函数体。这就是为什么可以在没有编译器的运行环境中,根据指针所指向的对象,去正确地调用该对象的功能函数。
5. 抽象类
5.1 为什么会需要抽象类
为什么会有 抽象类 这个概念呢?回顾我们学习类与对象的建模的时候,基类(父类)往往相对于其派生类(子类)对事物的描述更宽泛一些。也可以理解为,由于派生类的功能更强大使得它们相对能够完成更具体地任务,因此使得派生类们相对于他们地基类丢失了更多地自由度。比如,Mankind 是 Man 和 Woman 共同的基类,在派生时,不论成为 Man 还是 Woman 都必须附有其本身的特点完成分化。
派生就像生物学中造血干细胞分化的过程,但是有一种情况是 基类中代表某一类行为的函数,在子类中的表现不同,以至于基类中没有办法完全具体地描述这一类行为。
举个例子来说,在一个二维图形类中,定义一个 countArea()
方法,不便于直接求解这个二维图形的面积。由该二维图形类作为基类派生的更具体地图形类,如矩形类、三角形类、圆形类,都有各自简单地计算公式,这时可以不在基类(二维图形类)中给定 countArea()
方法地具体实现。
那么我们我为什么不索性直接不在基类中理会这样的函数呢?也不是不可以,但是这会导致在子类实中实现相同含义功能的函数有不同接口的风险。毕竟,我们在定义基类时就已经知道这个相同意义功能的函数在子类中都需要,因此选择使用抽象类的方式来 定义基类。
由于基类中存在没有实现的函数(见下一小节,纯虚函数),因此抽象类不能够进行实例化,而只用作其它类的父类(基类)。
5.2 纯虚函数 与 C++抽象类
纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:
virtual 函数类型 函数名(参数表) = 0;
在 C++ 中,带有 纯虚函数 的类称为 抽象类。因此,在 C++ 中的一个抽象类看起来包含这样的格式:
class 类名 { virtual 类型 函数名(参数表)=0; //其他成员…… }