课件
11.1 引言
int i = 3;OBJ o = OBJ();// 这里的 = 可以处理不同类型数据,可以看成是一种多态!Base* b = new Derived1();b->Print();// 这里的 b 指向其中的一个派生类对象,并通过基类指针访问派生类成员。Base* b = new Derived2();b->Print();// 这里的 b 指向其中的一个派生类对象,并通过基类指针访问派生类成员。// 看起来都是 b->Print();但实际输出其实是不同的,这种也是多态的一种形式// 使用起来没有区别,但是其实功能是不同的
在 C++ 中,多态指的是在不同的对象上调用相同的函数,但是这些函数在不同的对象上可以有不同的实现方式,这样就能实现在运行时决定使用哪种实现方式的特性。
C++ 中实现多态的方法主要有两种:
- 虚函数
- 模板
通过使用多态,可以增强程序的可扩展性和可维护性,提高程序的复用性和灵活性。
11.2 运算符重载
运算符重载-1
运算符重载-2
notes
运算符重载 Part I
回顾:
- 数据抽象 类
- 继承 类派生:能够容易地定义与其他类相似但又不相同的新类,能更容易地编写忽略这些相似类型之间区别的程序。
- 动态绑定:编译器能在运行时决定是使用基类中定义的函数还是派生类中定义的函数
C++ 中的运算符重载允许用户为自定义类型的对象定义运算符行为,以支持 更加直观的操作语义。
多态性的分类:
- 编译时的多态:
- 函数重载
- 运算符重载
- 运行时的多态:
- 虚函数
运算符重载的引入:
- 使用 C++ 编写程序时,我们不仅要使用基本数据类型,还要设计新的数据类型 —— 类类型
- 一般情况下,基本数据类型的运算都是用运算符来表达,这很直观,语义也简单。例如:
int a, b, c; a = b + c;
int a, b, c; a = b + c;mov eax,dword ptr[ebp-4]add eax,dword ptr[ebp-8]mov dword ptr[ebp-0Ch],eaxfloat a, b, c; c = a + b;fld dwordptr[ebp-4]fadd dwordptr[ebp-8]fstp dwordptr[ebp-0Ch]
如果直接将运算符作用在类类型之上,情况又如何呢?例如:Complex ret, c1, c2; ret = c1 + c2;
- 编译器将不能识别运算符的语义
- 需要一种机制来重新定义运算符作用在类类型上的含义
- 这种机制就是 运算符重载
#include <iostream>using namespace std;class Complex {double re, im;// real 实数(实部)// imaginary 虚数(虚部)public:Complex(double r = 0.0, double i = 0.0) : re(r), im(i) {}Complex add(Complex c) {Complex t;t.re = re + c.re;t.im = im + c.im;cout << "add result: "<< "(" << t.re << ", " << t.im << ")" << endl;return t;}};int main() {Complex c1(1, 2), c2(3, 4);Complex c3 = c1.add(c2); // => add result: (4, 6)return 0;}
c3 = c1.add(c2); 这种方式不直观,我们更希望这么写:c3 = c1 + c2;
复数?
复数是数学中的一个概念,通常表示为 的形式,其中
和
均为实数,
是虚数单位,满足
。一个复数
可以分解为实部
和虚部
的和:
。其中,
表示实数部分,
表示虚数部分,
表示虚数单位。
复数在数学、物理、工程学等领域中有广泛的应用,以下是一些例子:
- 解决无法用实数解决的方程
有些方程无法用实数解决,但是可以用复数解决。例如,二次方程在实数范围内无解,但是在复数范围内有两个解:
,其中
。复数可以帮助解决更复杂的方程。
- 描述电路中的信号
在电路中,信号可以通过复数来描述。复数的实部可以表示信号的振幅,虚部可以表示相位差。 - 描述波函数
在量子力学中,波函数描述了一个粒子的状态。波函数通常是一个复数函数,实部表示粒子的实际位置,虚部表示粒子的相位。 - 控制工程
在控制工程中,复数可以用于描述振荡的幅度和频率。控制工程师可以使用复数来帮助设计机器人、自动控制系统、飞机等等。
#include <iostream>using namespace std;class Complex {double re, im;public:Complex(double r = 0.0, double i = 0.0) : re(r), im(i) {}// 将运算符重载为成员函数形式Complex operator+(Complex c) { // 称 operator+(…) 为运算符重载函数Complex t;t.re = re + c.re;t.im = im + c.im;cout << "add result: "<< "(" << t.re << ", " << t.im << ")" << endl;return t;}};int main() {Complex c1(1, 2), c2(3, 4);Complex c3 = c1 + c2; // => add result: (4, 6)// c3 = c1 + c2; 称为 operator+(…) 函数的隐式调用c3 = c1.operator+(c2); // => add result: (4, 6)// c3 = c1 + c2; 等效于 c3 = c1.operator+(c2); 后者这种写法称为 operator+(…) 函数的显示调用return 0;}
#include <iostream>using namespace std;class Complex {double re, im;public:Complex(double r = 0.0, double i = 0.0) : re(r), im(i) {}// 将运算符重载为成员函数形式Complex operator+(Complex c) { // 称 operator+(…) 为运算符重载函数Complex t;t.re = re + c.re;t.im = im + c.im;cout << "add result: "<< "(" << t.re << ", " << t.im << ")" << endl;return t;}};int main() {Complex c1(1, 2), c2(3, 4);Complex c3 = c1 + c2; // => add result: (4, 6)c1 = c1 + 27; // => add result: (28, 2)// c1 = 27 + c1; // 错误return 0;}
c1 = c1 + 27;正确,相当于c1 = c1.operator+(Complex(27))c1 = 27 + c1;错误,被理解为无意义的c1 = 27.operator+(c1)
#include <iostream>using namespace std;class Complex {double re, im;public:Complex(double r = 0.0, double i = 0.0) : re(r), im(i) {}friend Complex operator+(Complex c1, Complex c2);};// 将运算符重载为成员函数形式Complex operator+(Complex c1, Complex c2) {Complex t;t.re = c1.re + c2.re;t.im = c1.im + c2.im;cout << "add result: "<< "(" << t.re << ", " << t.im << ")" << endl;return t;}int main() {Complex c1(1, 2), c2(3, 4);Complex c3 = c1 + c2; // => add result: (4, 6)c1 = c1 + 27; // => add result: (28, 2)c1 = 27 + c1; // => add result: (55, 2)return 0;}
将 27 隐式转换为 Complex 类型
c1 = c1 + 27;正确,相当于c1 = operator+(Complex(27), c1)c1 = 27 + c1;正确,相当于c1 = operator+(c1, Complex(27))
如果要输出复数对象,如何办?
#include <iostream>using namespace std;class Complex {double re, im;public:Complex(double r = 0.0, double i = 0.0) : re(r), im(i) {}friend Complex operator+(Complex c1, Complex c2);double getRe() { return re; }double getIm() { return im; }};Complex operator+(Complex c1, Complex c2) {Complex t;t.re = c1.re + c2.re;t.im = c1.im + c2.im;cout << "add result: "<< "(" << t.re << ", " << t.im << ")" << endl;return t;}int main() {Complex obj(3, 4);cout << obj.getRe() << "+" << obj.getIm() << "i" << endl; // => 3+4ireturn 0;}
如果希望如下方式,如何办?
cout << obj << endl;
#include <iostream>using namespace std;class Complex {double re, im;public:Complex(double r = 0.0, double i = 0.0) : re(r), im(i) {}friend Complex operator+(Complex c1, Complex c2);friend ostream& operator<<(ostream& out, Complex& obj);};ostream& operator<<(ostream& out, Complex& obj) {out << obj.re << "+" << obj.im << "i";return out;}Complex operator+(Complex c1, Complex c2) {Complex t;t.re = c1.re + c2.re;t.im = c1.im + c2.im;cout << "add result: "<< "(" << t.re << ", " << t.im << ")" << endl;return t;}int main() {Complex obj(3, 4);cout << obj << endl; // => 3+4ireturn 0;}
11.8.2 练习 | 运算符重载
为计数器 Counter 类重载 ++ -- () << 等运算符,能达到如下要求:
void main() {Counter acounter(5);++acounter; // 对计数器进行加 1 操作--acounter; // 对计数器进行减 1 操作int v = acounter(); // 获取计数器的值cout << acounter; // 输出计数器的值}
#include <iostream>using namespace std;class Counter {private:int count;public:Counter(int c = 0) : count(c) {}// 前缀递增运算符 ++Counter operator++() {++count;cout << "++count: " << count << endl;return Counter(count);}// 前缀递减运算符 --Counter operator--() {--count;cout << "--count: " << count << endl;return Counter(count);}// 无参数函数调用运算符 ()int operator()() const {cout << "get count: " << count << endl;return count;}// 输出运算符 <<friend ostream& operator<<(ostream& out, const Counter& c) {out << "Counter = " << c.count << endl;return out;}};int main() {Counter acounter(5);++acounter; // => ++count: 6--acounter; // => --count: 5int v = acounter(); // => get count: 5cout << acounter; // => Counter = 5cout << "v = " << v << endl; // => v = 5return 0;}
C++ 中的运算符重载时的参数传递规则
在运算符重载函数中,参数传递规则与普通成员函数类似,但有一些需要注意的细节。对于运算符重载,正确的参数传递规则能够提高程序的效率和可读性,从而使代码更加优美和易于维护。
- 参数个数
大多数运算符都只有一个或两个操作数,因此运算符重载函数的参数个数一般为一个或两个,但也有例外,例如函数调用运算符 () 可以有任意多个参数。
- 参数类型
参数类型应该和重载的运算符对应的操作数类型相同或兼容。对于二元运算符,第一个参数通常是调用对象的类型,而第二个参数是运算符右侧的操作数类型。对于一元运算符,只有一个参数,通常为调用对象的类型。
- 参数传递方式
C++ 中的参数传递方式有值传递、引用传递和指针传递三种方式。在运算符重载函数中,参数传递方式会影响操作数的复制方式,从而影响函数的执行效率和运算符的行为。
- 值传递:在函数调用时将实参的值复制到形参中,修改形参的值不会影响实参的值。
- 引用传递:在函数调用时将实参的引用传递给形参,形参和实参指向同一块内存,修改形参的值会影响实参的值。
- 指针传递:在函数调用时将实参的地址传递给形参,形参和实参指向同一块内存,修改形参的值会影响实参的值。
对于重载运算符,通常使用引用传递或常引用传递来避免不必要的复制。例如,在二元运算符 + 中,常常将第二个参数设为引用或常引用,这样可以避免对操作数的不必要复制,从而提高效率。
- const 关键字
在运算符重载函数中,使用 const 关键字可以增加代码的可读性和安全性。对于一元运算符,将重载函数声明为 const 类型可以让编译器知道该函数不会修改调用对象的值,从而允许使用 const 对象调用该函数。对于二元运算符,将第二个参数声明为 const 类型可以避免对操作数的修改。
运算符重载 Part II
下面我们实现一个计数器,能够实现计数器的初始化(缺省用 0),自增 ++,自减 --,取值 (),输出 << 等操作
分析:
- 类对象要实现自增,自减操作,也要进行运算符重载。
- 那么,如何区别前置和后置呢?
- 自增自减的函数原型是什么?
class Counter {int value;public:Counter(int v = 0) { value = v; }void operator++(); // 前缀void operator--(); // 前缀int operator()();friend ostream& operator<<(ostream& out, Counter& obj);};void Counter::operator++() { value++; }void Counter::operator--() { value--; }int Counter::operator()() { return value; }ostream& operator<<(ostream& out, Counter& obj) {out << obj.value;return out;};
如果希望有如下方式,又如何办?
acounter++;acounter--;
class Counter {int value;public:Counter(intv = 0) { value = v; }// ……Counter operator++(int); // 后缀Counter operator--(int); // 后缀};
为支持后缀用法,运算符重载函数再增加一个 int 类型的参数,称作 占位参数。
#include <iostream>using namespace std;class Counter {int value;public:Counter(int v = 0) { value = v; }Counter operator++(); // 前置 ++Counter operator--(); // 前置 --int operator()();friend ostream& operator<<(ostream& out, Counter& obj);Counter operator++(int) { // 后置 ++cout << "后置 ++" << endl;Counter tmp = *this; // 开辟一块新空间value = value + 1;return tmp;}Counter operator--(int) { // 后置 --cout << "后置 --" << endl;Counter tmp = *this;value = value - 1;return tmp;}};ostream& operator<<(ostream& out, Counter& obj) {out << obj.value;return out;};Counter Counter::operator++() {cout << "前置 ++" << endl;value = value + 1;Counter tmp = *this;return tmp;}Counter Counter::operator--() {cout << "前置 --" << endl;value = value - 1;Counter tmp = *this;return tmp;}int Counter::operator()() { return value; }int main() {Counter counter1(5);cout << "Counter counter2 = counter1--;" << endl;Counter counter2 = counter1--;cout << "counter1() => " << counter1() << endl; // => counter1() => 4cout << "counter2() => " << counter2() << endl; // => counter2() => 5cout << "Counter counter3 = counter2++;" << endl;Counter counter3 = counter2++;cout << "counter2() => " << counter2() << endl; // => counter2() => 6cout << "counter3() => " << counter3() << endl; // => counter3() => 5cout << "Counter counter4 = ++counter2;" << endl;Counter counter4 = ++counter2;cout << "counter2() => " << counter2() << endl; // => counter2() => 7cout << "counter4() => " << counter4() << endl; // => counter4() => 7cout << "Counter counter5 = --counter3;" << endl;Counter counter5 = --counter3;cout << "counter3() => " << counter3() << endl; // => counter3() => 4cout << "counter5() => " << counter5() << endl; // => counter5() => 4return 0;}/* 运行结果:Counter counter2 = counter1--;后置 --counter1() => 4counter2() => 5Counter counter3 = counter2++;后置 ++counter2() => 6counter3() => 5Counter counter4 = ++counter2;前置 ++counter2() => 7counter4() => 7Counter counter5 = --counter3;前置 --counter3() => 4counter5() => 4*/
对比:Counter operator++(int); void operator++(); 两种写法之间的差异
这两种写法都是运算符重载中常用的自增运算符重载形式。区别在于运算符重载函数的返回值类型和参数列表不同:
Counter operator++(int)表示 后缀自增运算符重载。其中的 int 参数是一个占位符,只是用来区分前缀和后缀自增运算符。在实际使用中,我们并不需要使用这个参数,因此可以省略名称。后缀自增运算符会返回一个旧值,并在完成运算后自增。void operator++()表示 前缀自增运算符重载。前缀自增运算符重载没有参数,它会直接改变自身的值,并返回一个引用。
#include <iostream>using namespace std;class Counter {int value;public:Counter(int v = 0) { value = v; }friend Counter operator++(Counter obj);int GetVal() { return value; };};Counter operator++(Counter obj) { // 直接传递对象,会自动生成一个全新的 Counter 对象obj.value++;return obj;}int main() {Counter counter1(5);Counter counter2 = ++counter1; // 此时并未改变 counter1 的值cout << "couter1 value = " << counter1.GetVal() << endl; // => couter1 value = 5cout << "couter2 value = " << counter2.GetVal() << endl; // => couter2 value = 6return 0;}
Q:为什么 counter1 的值没有发生变化?
传递的是 Counter 类的对象,改变的是形参 obj 的值
在上面的代码中,operator++ 函数接受一个 Counter 类型的参数,该参数按值传递,这意味着 operator++ 函数中对参数的修改不会影响传递给函数的实参对象。当执行 Counter counter2 = ++counter1; 时,operator++ 函数返回的是一个 新的 Counter 对象,而不是原始的 counter1 对象。
#include <iostream>using namespace std;class Counter {int value;public:Counter(int v = 0) { value = v; }friend Counteroperator++(Counter& obj); // 通过传递引用,可以改变对象的内部状态int GetVal() { return value; };};Counter operator++(Counter& obj) {obj.value++;return obj;}int main() {Counter counter1(5);Counter counter2 = ++counter1;cout << "couter1 value = " << counter1.GetVal()<< endl; // => couter1 value = 6cout << "couter2 value = " << counter2.GetVal()<< endl; // => couter2 value = 6++counter1;cout << "couter1 value = " << counter1.GetVal()<< endl; // => couter1 value = 7cout << "couter2 value = " << counter2.GetVal()<< endl; // => couter2 value = 6return 0;}
#include <iostream>using namespace std;class Counter {int value;public:Counter(int v = 0) { value = v; }friend Counter operator++(Counter& obj);friend Counter operator++(Counter& obj, int);int GetVal() { return value; };};Counter operator++(Counter& obj) {obj.value++;return obj;}Counter operator++(Counter& obj, int) {Counter tmp = obj;obj.value++;return tmp;}int main() {Counter counter1(3);Counter counter2 = ++counter1;Counter counter3 = counter1++;cout << "couter1 value = " << counter1.GetVal()<< endl; // => couter1 value = 5cout << "couter2 value = " << counter2.GetVal()<< endl; // => couter2 value = 4cout << "couter3 value = " << counter3.GetVal()<< endl; // => couter3 value = 4counter2++;cout << "couter1 value = " << counter1.GetVal()<< endl; // => couter1 value = 5cout << "couter2 value = " << counter2.GetVal()<< endl; // => couter2 value = 5cout << "couter3 value = " << counter3.GetVal()<< endl; // => couter3 value = 4counter3++;cout << "couter1 value = " << counter1.GetVal()<< endl; // => couter1 value = 5cout << "couter2 value = " << counter2.GetVal()<< endl; // => couter2 value = 5cout << "couter3 value = " << counter3.GetVal()<< endl; // => couter3 value = 5return 0;}
两种重载函数的比较
多数情况下,运算符可以重载为类的成员函数,也可以重载为友元函数。但两种重载也有各自特点:
- 一般情况下,单目运算符 重载为类的成员函数;双目元素 重载为类的友元函数
- 有些双目运算符 不能重载为类的友元函数:
=()[]-> - 类型转换函数 只能定义为类的 成员 函数,而不能定义为友元函数
- 若一个运算符的操作需要 修改对象的状态,则重载为 成员 函数比较好
- 若运算符所需要的操作数(尤其是 第一个操作数)希望有 隐式类型转换,则只能选择 友元 函数
- 若运算符是成员函数,最左边的操作数必须是运算符类的类对象(或者类对象的引用)。如果左边操作数必须是一个不同类的对象,或者是基本数据类型,则必须重载为友元函数
- 当需要重载运算符的元素具有 交换性 时,重载为 友元 函数
缺省的赋值运算符重载函数
class Complex {double re, im;// 如果没有为类重载赋值运算符,那么编译器会生成一个缺省的赋值运算符函数,其作用是通过位拷贝的方式将源对象复制到目的对象。public:Complex(double r = 0.0, double i = 0.0) : re(r), im(i) {}// ……};int main() {Complex c1(1, 2), c2(3, 4), c3;Complex c4 = c1 + c2; // 调用的是缺省拷贝构造函数初始化 c4c3 = c1 + c2; // 调用的是缺省的赋值运算符重载函数来改变 c3return 0;}
#include <iostream>using namespace std;class Complex {double re, im;public:Complex(double r = 0.0, double i = 0.0) : re(r), im(i) {}Complex(const Complex& c) : re(c.re), im(c.im) { cout << "1" << endl; }Complex& operator=(const Complex& c) {cout << "operator= called" << endl;if (this != &c) {re = c.re;im = c.im;}return *this;}Complex operator+(const Complex& c) {Complex t;t.re = re + c.re;t.im = im + c.im;return t;}friend ostream& operator<<(ostream& out, const Complex& c);};ostream& operator<<(ostream& out, const Complex& c) {out << c.re << "+" << c.im << "i";return out;}int main() {Complex c1(1, 2), c2(3, 4), c3;Complex c4 = c1 + c2; // 调用的是缺省拷贝构造函数初始化 c4c3 = c1 + c2; // => operator= calledcout << "c1 = " << c1 << endl; // => c1 = 1+2icout << "c2 = " << c2 << endl; // => c2 = 3+4icout << "c3 = " << c3 << endl; // => c3 = 4+6icout << "c4 = " << c4 << endl; // => c4 = 4+6iComplex c5(c1); // => 1cout << "c5 = " << c5 << endl; // => c5 = 1+2ireturn 0;}
赋值运算符函数与拷贝构造函数的异同
- 相同点:都是为了将一个对象的数据成员复制到另一个对象中
- 不同点:拷贝构造函数是要初始化一个新对象,而赋值运算符函数是要改变一个已经存在的对象
#include <iostream>#include <string.h>using namespace std;class CString {private:int len;char* buf;public:CString(int n);CString(CString& obj);~CString();void copy(const char* src);friend ostream& operator<<(ostream& out, CString& obj);};CString::CString(int n) {len = n;buf = new char[n];}void CString::copy(const char* src) { strcpy(buf, src); }CString::~CString() { delete[] buf; }CString::CString(CString& obj) {len = obj.len;buf = new char[len];strcpy(buf, obj.buf);}ostream& operator<<(ostream& out, CString& obj) {out << obj.buf << endl;return out;}void func() {CString obj1(64), obj2(32);obj1.copy("helloworld");CString obj3 = obj1;obj2 = obj3;// obj2 的 buf 成员指向的 32 字节的内存区将会丢失// obj2 和 obj3 的 buf 指向同一块内存区cout << obj1 << obj2 << obj3;}int main() {func();return 0;}/* 运行结果:helloworldhelloworldhelloworldfree(): double free detected in tcache 2Aborted (core dumped) */
free(): double free detected in tcache 2 Aborted (core dumped)
这段程序的内存管理逻辑存在问题。在 CString 类的拷贝构造函数和赋值运算符函数中,会进行动态内存分配。但是,没有定义移动构造函数和移动赋值运算符函数,导致在进行对象拷贝和赋值时,会进行深拷贝,从而导致对象的内存被多次释放,从而发生了 double free 的错误。
#include <cstdio>#include <iostream>#include <string.h>using namespace std;class CString {private:int len;public:char* buf; // 为了方便调试,直接通过对象获取 bufCString(int n);CString(CString& obj);~CString();void copy(const char* src);friend ostream& operator<<(ostream& out, CString& obj);};CString::CString(int n) {len = n;buf = new char[n];}void CString::copy(const char* src) { strcpy(buf, src); }CString::~CString() { delete[] buf; }CString::CString(CString& obj) {cout << "CString::CString(CString& obj) called" << endl;len = obj.len;buf = new char[len];strcpy(buf, obj.buf);}ostream& operator<<(ostream& out, CString& obj) {out << obj.buf << endl;return out;}void func() {CString obj1(64), obj2(32);obj1.copy("helloworld");CString obj3 = obj1;printf("obj1.buf => %p\n", obj1.buf);printf("obj3.buf => %p\n", obj3.buf);obj2 = obj3;printf("obj2.buf => %p\n", obj2.buf);printf("obj3.buf => %p\n", obj3.buf);cout << obj1 << obj2 << obj3;}int main() {func();return 0;}/* 运行结果:CString::CString(CString& obj) calledobj1.buf => 0x1215eb0obj3.buf => 0x1216340obj2.buf => 0x1216340obj3.buf => 0x1216340helloworldhelloworldhelloworldfree(): double free detected in tcache 2Aborted (core dumped) */

CString obj3 = obj1; 该语句在执行时,走的是 CString::CString(CString& obj) { } 构造函数,此时生成了一个新的 obj3,并且 obj3.buf 也是一块新的内存空间。obj2 = obj3; 该语句在执行时,obj2.buf 也将指向 obj3.buf,buj2.buf 原先指向的 32 字节的内存将被释放,相当于这块空间丢失了。
main 函数执行完毕,程序退出时,obj2.buf、obj3.buf 指向同一块内存,这块内存将会被释放 2 次 double free,导致内存被重复释放,进而导致程序崩溃。

#include <cstdio>#include <iostream>#include <string.h>using namespace std;class CString {private:int len;public:char* buf; // 为了方便调试,直接通过对象获取 bufCString(int n);CString(CString& obj);~CString();void copy(const char* src);friend ostream& operator<<(ostream& out, CString& obj);CString operator=(CString& from) {cout << "CString operator=(CString& from) called" << endl;if (this == &from)return *this; // 避免自己给自己赋值delete[] buf;len = from.len;buf = new char[len];strcpy(buf, from.buf);return *this;}};CString::CString(int n) {len = n;buf = new char[n];}void CString::copy(const char* src) { strcpy(buf, src); }CString::~CString() { delete[] buf; }CString::CString(CString& obj) {cout << "CString::CString(CString& obj) called" << endl;len = obj.len;buf = new char[len];strcpy(buf, obj.buf);}ostream& operator<<(ostream& out, CString& obj) {out << obj.buf << endl;return out;}void func() {CString obj1(64), obj2(32);obj1.copy("helloworld");CString obj3 = obj1;printf("obj1.buf => %p\n", obj1.buf);printf("obj3.buf => %p\n", obj3.buf);obj2 = obj3;printf("obj2.buf => %p\n", obj2.buf);printf("obj3.buf => %p\n", obj3.buf);cout << obj1 << obj2 << obj3;}int main() {func();return 0;}/* 运行结果:CString::CString(CString& obj) calledobj1.buf => 0xda8eb0obj3.buf => 0xda9340CString operator=(CString& from) calledCString::CString(CString& obj) calledobj2.buf => 0xda9390obj3.buf => 0xda9340helloworldhelloworldhelloworld */
重载运算符的几点注意事项
- 大多数预定义的运算符可以被重载,重载后的优先级、结合性、及所需的操作数都不变。
- 少数的 C++ 运算符不能重载,例如:
::#?:..** - 不能重载 非运算符 的符号,例如:
; - C++ 不允许重载 不存在的运算符,如
$**等 - 当运算符被重载时,它是被绑定在一个特定的类类型之上的。当此运算符不作用在特定类类型上时,它将保持原有的含义
- 当重载运算符时,不能创造新的运算符符号,例如不能用
**来表示求幂运算符 - 应当尽可能保持重载运算符原有的语义。试想,如果在某个程序中用
+表示减,*表示除,那么这个程序读起来将会非常别扭。
11.3 虚函数
虚函数
虚函数机制
notes
书店卖书
这个 demo 的目的:引出后续要介绍的“虚函数”
书店卖书,有 2 种不同情况:普通客户按照书的价格卖书,团购客户如果达到团购最低数量,则给予相应的折扣。请写函数实现卖书功能。
卖书有两种情况:正常卖书与折扣卖书。折扣卖书在正常卖书的基础上增加折扣功能,因此可以采用继承方法实现正常卖书与折扣卖书两个类。
#include <cstdio>#include <iostream>#include <string.h>using namespace std;class book_base {private:string isbn;protected:float price;public:book_base(string sales_isbn, float sales_price): isbn(sales_isbn), price(sales_price){};float net_price(int n) { return (n * price); }};class bulk_item : public book_base {private:int min_qty;float discount;public:bulk_item() : book_base("", 0.0), min_qty(0), discount(0.0){};bulk_item(string sales_isbn, float sales_price, int qty = 0,float dis_rate = 0.0): book_base(sales_isbn, sales_price), min_qty(qty),discount(dis_rate){};float net_price(int n) { return (n * price * (1 - discount)); }};int main() {book_base basebook("123", 12.5);bulk_item bulkbook("123", 12.5, 30, 0.1);cout << basebook.net_price(50) << endl; // => 625cout << bulkbook.net_price(50) << endl; // => 562.5return 0;}
注解说明
- book_base 类包含 isbn 和 price 两个成员变量和 net_price() 成员函数,表示普通的卖书方式。
book_base basebook("123", 12.5);新建一个 book_base 对象 basebook,表示普通卖书方式,书号是 123,一本书的单价为 12.5 元。basebook.net_price(50);使用普通卖书的方式,买 50 本书,返回这 50 本书的费用。
- bulk_item 类继承自 book_base 类,并新增了 min_qty 和 discount 两个成员变量,分别表示达到最低数量时的阈值和折扣率。
- 在实现 bulk_item 类时,需要在构造函数的初始化列表中调用基类 book_base 的构造函数。
- bulk_item 类重载了 net_price() 函数,实现折扣计算时需要考虑原价和折扣率两个因素。
bulk_item bulkbook("123", 12.5, 30, 0.1);新建一个 bulk_item 对象 bulkbook,表示团购卖书方式,书号是 123,一本书的单价为 12.5 元,团购书籍最低要达到 30 本才能有折扣,折扣率是 0.1 相当于打九折。bulkbook.net_price(50)使用团购卖书的方式,买 50 本书,返回这 50 本书的费用。
在主函数中,先分别实例化 book_base 类和 bulk_item 类的对象,并通过调用 net_price() 函数来计算卖出指定数量的书籍的总价格。最后输出计算结果。
相关术语
ISBN是国际标准书号(International Standard Book Number)的缩写,是一种图书的唯一标识符。它由 13 位数字组成,用于区分不同的图书、不同的版本和不同的印刷商net price表示“净价”,也就是商品的实际售价min_qty的全称是 minimum quantity 表示最小数量discount表示折扣率bulk_item表示以大宗销售的物品
#include <cstdio>#include <iostream>#include <string.h>using namespace std;class book_base {private:string isbn;protected:float price;public:book_base(string sales_isbn, float sales_price): isbn(sales_isbn), price(sales_price){};float net_price(int n) { return (n * price); }};class bulk_item : public book_base {private:int min_qty;float discount;public:bulk_item() : book_base("", 0.0), min_qty(0), discount(0.0){};bulk_item(string sales_isbn, float sales_price, int qty = 0,float dis_rate = 0.0): book_base(sales_isbn, sales_price), min_qty(qty),discount(dis_rate){};float net_price(int n) { return (n * price * (1 - discount)); }};float total_price(book_base& book, int n) { return book.net_price(n); }float total_price(bulk_item& book, int n) { return book.net_price(n); }int main() {book_base basebook("123", 12.5);bulk_item bulkbook("123", 12.5, 30, 0.1);cout << total_price(basebook, 50) << endl; // => 625cout << total_price(bulkbook, 50) << endl; // => 562.5return 0;}
重载普通的成员函数的两种方式:
- 在同一个类中重载:重载函数是以参数特征区分的
- 派生类重载基类的成员函数:由于重载函数处在不同的类中,因此它们的原型可以完全相同。调用时使用
类名::函数名的方式加以区分
以上两种重载的匹配都是在编译的时候静态完成的。
#include <cstdio>#include <iostream>#include <string.h>using namespace std;class book_base {private:string isbn;protected:float price;public:book_base(string sales_isbn, float sales_price): isbn(sales_isbn), price(sales_price){};float net_price(int n) { return (n * price); }};class bulk_item : public book_base {private:int min_qty;float discount;public:bulk_item() : book_base("", 0.0), min_qty(0), discount(0.0){};bulk_item(string sales_isbn, float sales_price, int qty = 0,float dis_rate = 0.0): book_base(sales_isbn, sales_price), min_qty(qty),discount(dis_rate){};float net_price(int n) { return (n * price * (1 - discount)); }};float total_price(book_base& book, int n) { return book.net_price(n); }// 只有一个函数int main() {book_base basebook("123", 12.5);bulk_item bulkbook("123", 12.5, 30, 0.1);cout << total_price(basebook, 50) << endl; // => 625cout << total_price(bulkbook, 50) << endl; // => 625return 0;}
#include <cstdio>#include <iostream>#include <string.h>using namespace std;class book_base {private:string isbn;protected:float price;public:book_base(string sales_isbn, float sales_price): isbn(sales_isbn), price(sales_price){};virtual float net_price(int n) { return (n * price); }};class bulk_item : public book_base {private:int min_qty;float discount;public:bulk_item() : book_base("", 0.0), min_qty(0), discount(0.0){};bulk_item(string sales_isbn, float sales_price, int qty = 0,float dis_rate = 0.0): book_base(sales_isbn, sales_price), min_qty(qty),discount(dis_rate){};float net_price(int n) { return (n * price * (1 - discount)); }};float total_price(book_base& book, int n) { return book.net_price(n); }int main() {book_base basebook("123", 12.5);bulk_item bulkbook("123", 12.5, 30, 0.1);cout << total_price(basebook, 50) << endl; // => 625cout << total_price(bulkbook, 50) << endl; // => 562.5return 0;}
切片
Base Bobj;Derived Dobj; // 派生类拥有从基类继承过来的成员Bobj = Dobj; // 派生类的对象可以直接赋值给基类的对象Base& refB = Dobj; // 基类对象的引用可以引用一个派生类对象Base* pB = &Dobj; // 基类对象的指针可以指向一个派生类对象
当一个派生类对象直接赋值给基类对象时,不是所有的数据都赋给了基类对象,赋予的只是派生类对象的一部分。这部分叫做派生类对象的 “切片”(sliced)。

回忆一下不同的继承方式,子类对基类中的成员的访问权限:
| 基类 | 公有成员 | 私有成员 | 保护成员 |
|---|---|---|---|
| 公有派生类 | 公有成员 | 不可访问成员 | 保护成员 |
| 私有派生类 | 私有成员 | 不可访问成员 | 私有成员 |
| 保护派生类 | 保护成员 | 不可访问成员 | 保护成员 |
只有在 公有派生类 的情况下,才有可能出现“基类的公有成员变成派生类的公有成员”的情况。
#include <cstdio>#include <iostream>#include <string.h>using namespace std;class Base {public:void Print() { cout << "I am Base\n"; };};class Derived : public Base {public:void Print() { cout << "I am Derived\n"; };};int main() {Base base, *p;Derived derived;p = &base;// 调用的是基类的 Printp->Print(); // => I am Basep = &derived;// 调用的是基类的 Printp->Print(); // => I am Basereturn 0;}
通过基类引用或指针所能看到的是一个基类对象,派生类中的成员对于基类引用或指针来说是“不可见的”。比如 Derived::Print 对于 p 而言就是不可见的。
虚函数
我们能不能 “通过基类引用或指针来访问派生类的成员” 呢?
为了达到上述目的,我们可以利用 C++ 的 虚函数机制,将基类的 Print 说明为虚函数形式。这样就可以通 过基类引用或指针 p 来访问派生类中的 Print Derived::Print
#include <cstdio>#include <iostream>#include <string.h>using namespace std;class Base {public:virtual void Print() { cout << "I am Base\n"; };};class Derived : public Base {public:void Print() { cout << "I am Derived\n"; };};int main() {Base base, *p;Derived derived;p = &base;// 调用的是 基类 的 Printp->Print(); // => I am Basep = &derived;// 调用的是 派生类 的 Printp->Print(); // => I am Derivedreturn 0;}
#include <cstdio>#include <iostream>#include <string.h>using namespace std;class Base {public:virtual void Print() { cout << "I am Base\n"; };};class Derived1 : public Base {public:void Print() { cout << "I am Derived1\n"; };};class Derived2 : public Base {public:void Print() { cout << "I am Derived2\n"; };};int main() {Base base, *p;Derived1 obj1;Derived2 obj2;p = &base;// 调用的是 Base 的 Printp->Print(); // => I am Basep = &obj1;// 调用的是 Derived1 的 Printp->Print(); // => I am Derived1p = &obj2;// 调用的是 Derived2 的 Printp->Print(); // => I am Derived2return 0;}
虚函数小结
- 在基类中用 virtual 关键字声明的成员函数即为虚函数。
- 虚函数可以在一个或多个 公有派生类 中被重载,但要求在重载时 虚函数的原型(包括返回值类型、函数名、参数列表)必须完全相同。
- 定义基类引用或指针,使其引用或指向派生类对象。当通过该引用或指针调用虚函数时,该函数将体现出虚特性来。
- C++ 中,基类必须指出希望派生类重定义那些函数。定义为 virtual 的函数 是基类 期待派生类重新定义 的,基类希望派生类继承的函数不能定义为虚函数。
- 在派生类中重载虚函数时必须与基类中的函数原型相同,否则该函数将丢失虚特性。
- 函数原型不同,仅函数名相同。C++ 编译器认为这是一般的函数重载,此时虚特性丢失。
- 仅返回类型不同,其他相同。C++ 编译器认为这种情况是不允许的。
虚函数的动态绑定
思考:虚函数如何体现 动态绑定 的特性?
#include <iostream>#include <string.h>using namespace std;class Base {public:virtual void Print() { cout << "I am Base\n"; };};class Derived : public Base {public:void Print() { cout << "I am Derived\n"; };};int main() {Base base, *p;Derived derived;int condition;cin >> condition; // condition 的值依赖于程序运行起来后用户的输入// p 的初始化依赖于 condition 的值,只有运行时才能确定调用的是“谁”的 Print。if (1 == condition) p = &base;else p = &derived;p->Print();return 0;}/* 运行结果:1I am Base0I am Derived*/
提供虚函数的意义
- 提升软件的重用性
- 基类使用虚函数提供一个接口,但派生类可以定义自己的实现版本。
- 虚函数调用的解释依赖于它的对象类型,这就实现了“一个接口,多种语义”的概念。
- 提高软件架构的合理性
#include <iostream>#include <string.h>using namespace std;class Base {public:virtual void Print() { cout << "I am Base\n"; };};class Derived : public Base {public:void Print() { cout << "I am Derived\n"; };};// 将对 p 的初始化操作,封装到 Init 函数中void Init(Base*& p) {Base base;Derived derived;int condition;cin >> condition;if (1 == condition) p = &base;else p = &derived;}int main() {Base* p;Init(p);p->Print();return 0;}/* 运行结果:1I am Base0I am Derived*/
虚函数的实现机制
#include <iostream>#include <string.h>using namespace std;int add(int a, int b) { return a + b; }int sub(int a, int b) { return a - b; }int main() {int (*pfunction)(int, int);int ret;pfunction = add;ret = pfunction(5, 3);cout << "ret = " << ret << endl; // => ret = 8pfunction = sub;ret = pfunction(6, 3);cout << "ret = " << ret << endl; // => ret = 3return 0;}
虚函数表和虚指针
- 在编译时,为每个有虚函数的类建立一张 虚函数表 VTABLE,表中存放的是每一个虚函数的指针;同时用一个 虚指针 VPTR 指向这张表的入口。
- 访问某个虚函数时,不是直接找到那个函数的地址,而是通过 VPTR 间接查到它的地址。

对象的内存空间除了保存数据成员外,还保存 VPTR。VPTR 由构造函数来初始化。

- 虚函数必须是类的非静态成员函数。
- 不能将虚函数说明为全局函数。
- 不能将虚函数说明为静态成员函数。
- 不能将虚函数说明为友元函数。
- 本质的原因就是非静态成员函数隐含传递 this 指针,而通过 this 指针能够找到 VPTR
- 在一个基类或派生类的成员函数中,可以直接调用类等级中的虚函数。此时需要根据成员函数中 this 指针所指向的对象 来判断调用的是哪一个函数。
- 构造函数不能定义为虚函数
- 析构函数可以定义为虚函数。
- 若析构函数为虚函数,那么当使用 delete 释放基类指针所指向的派生类对象时,先调用派生类的析构函数,再调用基类的析构函数。
#include <iostream>#include <string.h>using namespace std;class Base {public:virtual ~Base() { cout << "Base destroy\n"; }};class Derived : public Base {public:~Derived() { cout << "Derived destroy\n"; }};int main() {Base *p1, *p2;p1 = new Base();p2 = new Derived();delete p1;delete p2;return 0;}/* 运行结果:Base destroyDerived destroyBase destroy*/
#include <iostream>#include <string.h>using namespace std;class Base {public:~Base() { cout << "Base destroy\n"; }};class Derived : public Base {public:~Derived() { cout << "Derived destroy\n"; }};int main() {Base *p1, *p2;p1 = new Base();p2 = new Derived();delete p1;delete p2;return 0;}/* 运行结果:Base destroyBase destroy*/
11.4 纯虚函数与抽象类
定义纯虚函数的语法形式:virtual type functionName(parameters) = 0;
#include <iostream>using namespace std;class Shape {public:virtual float Perimeter() = 0;virtual float Area() = 0;virtual ~Shape() {}};class Squre : public Shape {private:float l, h;public:Squre(float l, float h) : l(l), h(h) {}// 求周长float Perimeter() override { return 2 * (l + h); }// 求面积float Area() override { return l * h; }};class Circle : public Shape {private:float r;public:Circle(float r) : r(r) {}float Perimeter() override { return 2 * 3.14 * r; }float Area() override { return 3.14 * r * r; }};int main() {Shape* p;Circle circle(5.0);Squre squre(3.0, 4.0);p = &circle;cout << "Circle Area: " << p->Area() << endl; // => Circle Area: 78.5p = &squre;cout << "Squre Area: " << p->Area() << endl; // => Squre Area: 12return 0;}

小结:
- 基类中的这些公共接口只需要有说明而不需要有实现,即 纯虚函数。
- 纯虚函数刻画了派生类应该遵循的协议,这些协议的具体实现由派生类来决定。
- 纯虚函数的作用是 强制派生类实现该函数
- 将一个函数说明为纯虚函数,就要求任何派生类都 必须 定义自己的实现。
- 当抽象类的 所有函数成员都是纯虚函数 时,这个类被称为 接口类。
- 拥有纯虚函数的类被称为 抽象类
- 抽象类不能被实例化,只能作为基类被使用
- 抽象类的派生类需要实现纯虚函数,否则该派生类也是一个抽象类
- 继承和动态绑定在两个方面简化了我们的程序
- 能够容易地定义与其他类相似但又不相同的新类
- 能更容易地编写忽略这些相似类型之间区别的程序
- 许多应用程序的特性可以用一些相关但略有不同的概念描述。面向对象编程与这种应用非常匹配。通过 继承 可以定义一些类型,可以 模拟不同种类;通过 动态绑定 可以编写程序,使用这些类而又 忽略与具体类型相关的差异。
- 继承和动态绑定的思想在概念上非常简单,但对于如何创建应用程序以及对于程序设计语言必须支持的特性,含义深远。
- 面向对象编程的关键思想是多态性
- 通过继承而相关联的类型为多态类型
- 在许多情况下可以互换地使用派生类型或基类型的“许多形态”
- C++ 中,多态性仅用于通过继承而相关联的类型的引用或指针
- 我们称因继承而相关的类构成了一个继承层次。其中一个类称为根,所有其他类直接或间接继承根类
