★ 掌握运算符重载的语法和规则
★ 掌握运算符重载的方式
★ 掌握常用的运算符重载
★ 掌握类型转换函数的用法
★ 了解仿函数的实现方式
★ 了解智能指针的实现方式
C++的一大特性就是重载,重载使得程序更加简洁高效。在C++中不只函数可以重载,运算符也可以重载,运算符重载主要是面向对象之间的。本章将针对运算符重载的相关知识进行详细讲解。
在C++中,运算符的操作对象可以是基本的数据类型,也可以是类中重新定义的运算符,赋予运算符新的功能,对类对象进行相关操作。在类中重新定义运算符,赋予运算符新的功能以适应自定义数据类型的运算,就称为运算符重载。
前面章节学习中已经使用过重载的运算符,如运算符“+”可以进行算术的加法运算,在String类中可以连接两个字符串;运算符“>>”和“<<”可以对数据进行右移和左移运算,在输入、输出流类中可以实现输入和输出操作。本节将针对运算符重载的语法、规则和方式进行介绍。
在C++中,使用operator关键字定义运算符重载。运算符重载语法格式如下:
返回值类型 operator 运算符名称 (参数列表)
{
...//函数体
}
从运算符重载语法格式可以看出,运算符重载的返回值类型、参数列表可以是任意数据类型。除了函数名称中的operator关键字,运算符重载函数与普通函数没有区别。
下面通过案例演示“+”“−”运算符的重载,如例3-1所示。
例3-1 operator.cpp
1 #include<iostream>
2 using namespace std;
3 class A
4 {
5 private:
6 int _x;
7 int _y;
8 public:
9 A(int x=0,int y=0):_x(x),_y(y){}
10 void show() const; //输出数据
11 A operator+(const A& a) const; //重载"+"运算符
12 A operator-(const A& a) const; //重载"-"运算符
13 };
14 void A::show() const //show()函数的实现
15 {
16 cout<<"(_x,_y)="<<"("<<_x<<","<<_y<<")"<<endl;
17 }
18 A A::operator+(const A& a) const //重载"+"运算符的实现
19 {
20 return A(_x+a._x,_y+a._y);
21 }
22 A A::operator-(const A& a) const //重载"-"运算符的实现
23 {
24 return A(_x-a._x,_y-a._y);
25 }
26 int main()
27 {
28 A a1(1,2);
29 A a2(4,5);
30 A a;
31 cout<<"a1: ";
32 a1.show();
33 cout<<"a2: ";
34 a2.show();
35 a=a1+a2; //实现两个对象相加
36 cout<<"a: ";
37 a.show();
38 a=a1-a2; //实现两个对象相减
39 cout<<"a: ";
40 a.show();
41 return 0;
42 }
例3-1运行结果如图3-1所示。
例3-1中第18~21行代码重载了运算符“+”,第22~25行代码重载了运算符“−”。在m ain()函数中,第28~29行代码创建并初始化类A的对象a1和a2,第35行代码通过重载的运算符“+”实现对象a1、a2相加并将结果保存到对象a中,第38行代码通过重载的运算符“−”实现对象a1、a2相减并将结果保存到对象a中。由图3-1可知,重载后的“+”“−”运算符成功实现了两个对象的加减运算。

图3-1 例3-1运行结果
通过上面讲解可以知道,重载运算符并没有改变其原来的功能,只是增加了针对自定义数据类型的运算功能,具有了更广泛的多态特征。
3.1.1节讲解了运算符重载的语法格式,需要注意的是,有的运算符是不可以重载的,并且运算符重载不可以改变语义。运算符重载的具体规则如下。
表3-1 可重载的运算符

运算符重载一般有两种形式:重载为类的成员函数和重载为类的友元函数。下面分别对这两种形式进行详细讲解。
1. 重载为类的成员函数
在3.1.1节重载“+”“−”运算符为类的成员函数,成员函数可以自由地访问本类的成员。运算的操作数会以调用者或参数的形式表示。
如果是双目运算符重载为类的成员函数,则它有两个操作数:左操作数是对象本身的数据,由this指针指出;右操作数则通过运算符重载函数的参数列表传递。双目运算符重载后的调用格式如下所示:
左操作数.运算符重载函数(右操作数);
例3-1中重载“+”运算符,当调用a1+a2时,其实就相当于函数调用a1.oprerator+(a2)。
如果是单目运算符重载为类的成员函数,需要确定重载的运算符是前置运算符还是后置运算符。如果是前置运算符,则它的操作数是函数调用者,函数没有参数,其调用格式如下所示:
操作数.运算符重载函数();
例如重载单目运算符“++”,如果重载的是前置运算符“++”,则++a1的调用相当于调用函数a1.operator++()。如果重载的是后置运算符“++”,则运算符重载函数需要带一个整型参数,即“operator++(int)”,参数int仅仅表示后置运算,用于和前置运算区分,并无其他意义。
为了加深读者的理解,下面通过案例演示前置运算符“++”与后置运算符“++”的重载,如例3-2所示。
例3-2 operatorPlus.cpp
1 #include<iostream>
2 using namespace std;
3 class A
4 {
5 private:
6 int _x;
7 int _y;
8 public:
9 A(int x=0,int y=0):_x(x),_y(y){}
10 void show() const; //输出数据
11 A operator++(); //重载前置“++”
12 A operator++(int); //重载后置“++”
13 };
14 void A::show() const
15 {
16 cout<<"(_x,_y)="<<"("<<_x<<","<<_y<<")"<<endl;
17 }
18 A A::operator++() //前置“++”实现
19 {
20 ++_x;
21 ++_y;
22 return *this;
23 }
24 A A::operator++(int) //后置“++”实现
25 {
26 A a=*this;
27 ++(*this); //调用已经实现的前置“++”
28 return a;
29 }
30 int main()
31 {
32 A a1(1,2), a2(3,4);
33 (a1++).show();
34 (++a2).show();
35 return 0;
36 }
例3-2运行结果如图3-2所示。

图3-2 例3-2运行结果
在例3-2中,第11~12行代码分别在类A中声明前置“++”和后置“++”运算符重载函数。第18~23行代码在类外实现前置“++”运算符重载函数,在函数内部,类的成员变量进行自增运算,然后返回当前对象(即this指针所指向的对象)。第24~29行代码在类外实现后置“++”运算符重载函数,在函数内部,创建一个临时对象保存当前对象的值,然后再将当前对象自增,最后返回保存初始值的临时对象。第32~34行代码创建了两个对象a1、a2,a1调用后置“++”,a2调用前置“++”。由图3-2运行结果可知,对象a1先输出结果后执行“++”运算,而对象a2先执行“++”运算后输出结果。
2. 重载为类的友元函数
运算符重载为类的友元函数,需要在函数前加friend关键字,其语法格式如下所示:
friend 返回值类型 operator 运算符(参数列表)
{
...//函数体
}
重载为类的友元函数时,由于没有隐含的this指针,因此操作数的个数没有变化,所有的操作数都必须通过函数的参数进行传递,函数的参数与操作数自左至右保持一致。
下面通过案例演示将运算符“+”和“−”重载为类的友元函数,如例3-3所示。
例3-3 operatorFriend.cpp
1 #include<iostream>
2 using namespace std;
3 class A
4 {
5 private:
6 int _x;
7 int _y
8 public:
9 A(int x=0,int y=0):_x(x),_y(y){}
10 void show() const; //输出数据
11 friend A operator+(const A& a1, const A& a2) ; //重载为类的友元函数
12 friend A operator-(const A& a1, const A& a2); //重载为类的友元函数
13 };
14 void A::show() const
15 {
16 cout<<"(_x,_y)="<<"("<<_x<<","<<_y<<")"<<endl;
17 }
18 A operator+(const A& a1,const A& a2)
19 {
20 return A(a1._x+a2._x,a1._y+a2._y);
21 }
22 A operator-(const A& a1,const A& a2)
23 {
24 return A(a1._x-a2._x,a1._y-a2._y);
25 }
26 int main()
27 {
28 A a1(1,2);
29 A a2(4,5);
30 A a;
31 cout<<"a1: ";
32 a1.show();
33 cout<<"a2: ";
34 a2.show();
35 a=a1+a2;
36 cout<< "a: ";
37 a.show();
38 a=a1-a2;
39 cout<<"a: ";
40 a.show();
41 return 0;
42 }
例3-3运行结果如图3-3所示。

图3-3 例3-3运行结果
在例3-3中,第11~12行代码将“+”和“−”运算符重载函数声明为类A的友元函数。将运算符重载函数声明为类的友元函数,与例3-1重载为类的成员函数的用法和规则相同,在此不再赘述。
C++输入输出标准库提供了“>>”和“<<”运算符执行输入、输出操作,但标准库只定义了基本数据类型的输入、输出操作,若要直接对类对象进行输入、输出,则需要在类中重载这两个运算符。
与其他运算符不同的是,输入、输出运算符只能重载成类的友元函数。“<<”和“>>”运算符重载的格式如下:
ostream& operator<<(ostream&, const 类对象引用); //输出运算符重载 istream& operator>>(istream&, 类对象引用); //输入运算符重载
输出运算符“<<”重载的第一个参数是ostream对象引用,该对象引用不能使用const修饰,第二个参数是输出对象的const引用。输入运算符“>>”重载的第一个参数是istream对象引用,第二个参数是要向其中存入数据的对象,该对象不能使用const修饰。
下面通过案例演示输入/输出运算符重载的用法,如例3-4所示。
例3-4 operatorStream.cpp
1 #include<iostream>
2 using namespace std;
3 class A
4 {
5 private:
6 int _x;
7 int _y;
8 public:
9 A(int x=0,int y=0):_x(x),_y(y){}
10 friend ostream& operator<<(ostream& os,const A& a); //重载“<<”运算符
11 friend istream& operator>>(istream& is,A& a); //重载“>>”运算符
12 };
13 ostream& operator<<(ostream& os, const A& a)
14 {
15 os<<"("<<a._x<<","<<a._y<<")"; //输出类的数据成员
16 return os;
17 }
18 istream& operator>>(istream& is, A& a)
19 {
20 is>>a._x>>a._y; //输入类的成员数据
21 return is;
22 }
23 int main()
24 {
25 A a1(1,2);
26 cout<<"a1:"<<a1<<endl;
27 cout<<"请重新为a1对象输入数据:"<<endl;
28 cin>>a1;
29 cout<<"重新输入后a1:"<<a1<<endl;
30 return 0;
31 }
例3-4运行结果如图3-4所示。

图3-4 例3-4运算结果
在例3-4中,第13~17行代码重载了输出运算符“<<”,第18~22行代码重载了输入运算符“>>”。在main()函数中,第25行代码创建类A对象a1并初始化,第26行代码直接使用重载的输出运算符输出对象a1的值,第28行代码调用重载的输入运算符为a1对象重新赋值,第29行代码调用重载的输出运算符输出对象a1的值。
由图3-4运行结果可知,重载运算符“<<”和“>>”后,类对象可以和基本数据类型一样直接执行输入、输出操作,不用再编写例3-1中的show()成员函数,使程序更简洁。
关系运算符(如“==”或“<”)也可以重载,关系运算符的重载函数返回值类型一般定义为bool类型,即返回true或false。关系运算符常用于条件判断中,重载关系运算符保留了关系运算符的原有含义。
下面通过案例演示关系运算符的重载,如例3-5所示。
例3-5 operatorRelation.cpp
1 #include<iostream>
2 using namespace std;
3 class Student
4 {
5 private:
6 int _id;
7 double _score;
8 public:
9 Student(int id,double score):_id(id),_score(score){}
10 void dis()
11 {
12 cout<<"学号"<<_id<<"成绩"<<_score<<endl;
13 }
14 //重载关系运算符
15 friend bool operator==(const Student& st1,const Student& st2);
16 friend bool operator!=(const Student& st1,const Student& st2);
17 friend bool operator>(const Student& st1,const Student& st2);
18 friend bool operator<(const Student& st1,const Student& st2);
19 };
20 bool operator==(const Student& st1,const Student& st2)
21 {
22 return st1._score==st2._score; //重载“==”运算符
23 }
24 bool operator!=(const Student& st1,const Student& st2)
25 {
26 return !(st1._score==st2._score); //重载“!=”运算符
27 }
28 bool operator>(const Student& st1,const Student& st2)
29 {
30 return st1._score>st2._score; //重载“>”运算符
31 }
32 bool operator<(const Student& st1,const Student& st2)
33 {
34 return st1._score<st2._score; //重载“<”运算符
35 }
36 int main()
37 {
38 Student st1(1001,96),st2(1002,105);
39 cout<<"比较两名学生的成绩:"<<endl;
40 if(st1>st2)
41 st1.dis();
42 else if(st1<st2)
43 st2.dis();
44 else
45 cout<<"两名学生成绩相同:"<<endl;
46 return 0;
47 }例3-5运行结果如图3-5所示。

图3-5 例3-5运行结果
在例3-5中重载了四个典型的比较运算符,重载比较运算符后,可以直接比较对象的大小,而实际实现中只是比较了对象中的score数据。如果没有重载关系运算符,需要先通过一个公有函数访问获得score,然后再来比较score的大小。
关系运算符重载有以下几点使用技巧。
对于赋值运算符来说,如果不重载,类会自动提供一个赋值运算符。这个默认的赋值运算符和默认的拷贝构造函数一样,实现的是浅拷贝。若数据成员中有指针,则默认的赋值运算符不能满足要求,会出现重析构的现象,这时就需要重载赋值运算符,实现深拷贝。
赋值运算符的重载与其他运算符的重载类似。下面通过案例演示赋值运算符的重载,如例3-6所示。
例3-6 operatorAssign.cpp
1 #define _CRT_SECURE_NO_WARNINGS
2 #include<string.h>
3 #include<iostream>
4 using namespace std;
5 class Assign
6 {
7 public:
8 char* name;
9 char* url;
10 public:
11 Assign(const char* name,const char* url); //构造函数
12 Assign(const Assign& temp); //拷贝构造函数
13 ~Assign()
14 {
15 delete[]name;
16 delete[]url;
17 }
18 Assign& operator=(Assign& temp); //赋值运算符重载
19 };
20 Assign::Assign(const char* name,const char* url)
21 {
22 this->name=new char[strlen(name)+1];
23 this->url=new char[strlen(url)+1];
24 if(name)
25 strcpy(this->name,name);
26 if(url)
27 strcpy(this->url,url);
28 }
29 Assign::Assign(const Assign& temp)
30 {
31 this->name=new char[strlen(temp.name)+1];
32 this->url=new char[strlen(temp.url)+1];
33 if(name)
34 strcpy(this->name,temp.name);
35 if(url)
36 strcpy(this->url,temp.url);
37 }
38 Assign& Assign:: operator=(Assign& temp)
39 {
40 delete[]name;
41 delete[]url; //先释放原来空间,再重新申请
42 this->name=new char[strlen(temp.name)+1];
43 this->url=new char[strlen(temp.url)+1];
44 if(name)
45 strcpy(this->name,temp.name
46 if(url)
47 strcpy(this->url,temp.url);
48 return *this;
49 }
50 int main()
51 {
52 Assign a("传智播客", "http://net.itcast.cn/");
53 cout<<"a对象:" <<a.name<<" "<<a.url<<endl;
54 Assign b(a); //用a对象初始化b,调用的是拷贝构造函数
55 cout<<"b对象:" <<b.name<<" "<<b.url<<endl;
56 Assign c("黑马训练营", "http://www.itheima.com/");
57 cout<<"c对象:" <<c.name<<" "<<c.url<<endl;
58 b=c; //调用赋值重载函数
59 cout<<"b对象:"<<b.name<<" "<<b.url<<endl;
60 return 0;
61 }
例3-6运行结果如图3-6所示。

图3-6 例3-6运行结果
在例3-6中,类A ssign中含有指针数据成员,第38~49行代码在类外实现赋值运算符“=”重载函数。由于对象b已经存在,nam e和url指针所指区域范围大小已经确定,要复制新内容进去,则区域过大或过小都不好,因此重载赋值运算符时,需要内部先释放nam e、url指针,根据要复制的内容大小再分配一块内存区域,然后将内容复制进去。在m ain()函数中,第58行代码通过重载赋值运算符完成对对象b的赋值。
在程序设计中,通常使用下标运算符“[]”访问数组或容器中的元素。为了在类中方便地使用“[]”运算符,可以在类中重载运算符“[]”。重载“[]”运算符有两个目的:
(1)“对象[下标]”的形式类似于“数组[下标]”,更加符合用户的编写习惯。
(2)可以对下标进行越界检查。
重载下标运算符“[]”的语法格式如下所示:
返回值类型 operator[](参数列表)
{
...//函数体
}
上述格式中,“[]”运算符重载函数有且只有一个整型参数,表示下标值。重载下标运算符时一般把返回值指定为一个引用。
下面通过案例演示重载下标运算符“[]”的用法,如例3-7所示。
例3-7 operatorTag.cpp
1 #define _CRT_SECURE_NO_WARNINGS
2 #include<iostream>
3 using namespace std;
4 class Tag
5 {
6 private:
7 int size;
8 char* buf;
9 public:10 Tag(int n);
10 Tag(int n);
11 Tag(const char* src);
12 ~Tag()
13 {
14 delete[]buf;
15 }
16 char& operator[](int n);
17 void show()
18 {
19 for(int i=0;i<size;i++)
20 cout<<buf[i];
21 cout<<endl;
22 }
23 };
24 Tag::Tag(int n)
25 {
26 size=n;
27 buf=new char[size+1];
28 *(buf+size)='\0';
29 }
30 Tag::Tag(const char* src)
31 {
32 buf = new char[strlen(src)+1];
33 strcpy(buf,src);
34 size=strlen(buf);
35 }
36 char& Tag::operator[](int n)
37 {
38 static char ch=0;
39 if(n>size||n<0) //检查数组是否越界
40 {
41 cout<<"越界"<<endl;
42 return ch;
43 }
44 else
45 return *(buf+n);
46 }
47 int main()
48 {
49 Tag arr1(20);
50 for(int i=0;i<20;i++)
51 arr1[i]=65+i; //调用“[]”运算符重载函数赋值
52 arr1.show();
53 Tag arr2("Itcast!");
54 cout<<arr2[6]<<endl;
55 arr2[6]= 'A';
56 arr2.show();
57 return 0;
58 }
例3-7运行结果如图3-7所示。

图3-7 例3-7运行结果
在例3-7中,第4~23行代码定义了一个字符数组类Tag;第36~46行代码重载了“[]”运算符。在m ain()函数中,第49行代码创建字符数组对象arr1,指定数组大小为20;第50~51行代码通过“[]”运算符给数组赋值;第53行代码创建字符数组对象arr2并初始化;第55行代码调用“[]”运算符重载函数,对指定索引位置的字符元素进行修改。
基本数据类型的数据可以通过强制类型转换操作符将数据转换成需要的类型,例如static_cast<int>(3.14),这个表达式是将实型数据3.14转换成整型数据。对于自定义的类,C++提供了类型转换函数来实现自定义类与基本数据类型之间的转换。
对于自定义的类,C++提供了类型转换函数用来将类对象转换为基本数据类型。
类型转换函数也称为类型转换运算符重载函数,定义格式如下所示:
operator 数据类型名()
{
...//函数体
}
类型转换函数以operator关键字开头,这一点和运算符重载规律一致。从类型转换函数格式可以看出,在重载的数据类型名前不能指定返回值类型,返回值的类型由重载的数据类型名确定,且函数没有参数。由于类型转换函数的主体是本类的对象,因此只能将类型转换函数重载为类的成员函数。
下面通过案例演示类型转换函数的用法,如例3-8所示。
例3-8 operatorCast.cpp
1 #define _CRT_SECURE_NO_WARNINGS
2 #include<iostream>
3 using namespace std;
4 class Student
5 {
6 private:
7 string _id;
8 char* _name;
9 public:
10 Student(string id, const char* name) :_id(id)
11 {
12 _name = new char[strlen(name) + 1];
13 strcpy(_name, name);
14 }
15 operator char*() //类型转换运算符重载函数
16 {
17 return _name;
18 }
19 void show()
20 {
21 cout<<"ID:"<<_id<<","<<"name:"<<_name<<endl;
22 }
23 };
24 int main()
25 {
26 Student s1("1001","小明"); //调用普通构造函数创建对象
27 cout<<"s1: ";
28 s1.show();
29 char* ch=s1; //调用类型转换函数
30 cout<<ch<<endl;
31 return 0;
32 }
例3-8运行结果如图3-8所示。
在例3-8中,第15~18行代码定义了类型转换函数,用于将Student类的对象转换为char*类型;第29行代码通过调用重载的char*类型转换函数,将对象s1成功转换为了char*类型。

图3-8 例3-8运行结果
转换构造函数指的是构造函数只有一个参数,且参数不是本类的const引用。用转换构造函数不仅可以将一个标准类型数据转换为类对象,也可以将另一个类的对象转换为转换构造函数所在的类对象。转换构造函数的语法格式如下所示:
class A
{
A(const B & b)
{
//从B类类型到A类类型的转换
}
};
下面通过案例演示转换构造函数的用法,如例3-9所示。
例3-9 constructorCast.cpp
1 #include<iostream>
2 using namespace std;
3 class Solid
4 {
5 public:
6 Solid(int x,int y,int z) :_x(x), _y(y),_z(z){}
7 void show()
8 {
9 cout<<"三维坐标"<<_x<<","<<_y<<","<<_z<<endl;
10 }
11 friend class Point;
12 private:
13 int _x,_y,_z;
14 };
15 class Point
16 {
17 private:
18 int _x, _y;
19 public:
20 Point(int x, int y) :_x(x), _y(y){}
21 Point(const Solid &another) //定义转换构造函数
22 {
23 this->_x=another._x;
24 this->_y=another._y;
25 }
26 void show()
27 {
28 cout<<"平面坐标:"<<_x<<","<<_y<<endl;
29 }
30 };
31 int main()
32 {
33 cout<<"原始坐标"<<endl;
34 Point p(1,2);
35 p.show()
36 Solid s(3,4,5);
37 s.show();
38 cout<<"三维转换平面坐标"<<endl;
39 p=s;
40 p.show();
41 return 0;
42 }
例3-9运行结果如图3-9所示。
在例3-9中,第3~14行代码定义了表示三维坐标点的类Solid;第15~30行代码定义了表示平面坐标点的类Point,在Point类中定义了一个转换构造函数,将三维坐标点Solid类对象转换为平面坐标点Point类的数据。需要注意的是,由于需要在Point类中访问Solid的成员变量,因此将Solid类声明为Point类的友元类。

图3-9 例3-9运行结果
仿函数指的是在类中重载“()”运算符后,这个类的对象可以像函数一样使用。仿函数在STL的算法中使用比较广泛。此外,熟悉的lambda表达式在实现过程中也使用了仿函数。
下面通过案例演示重载“()”运算符的用法,如例3-10所示。
例3-10 operatorFunc.cpp
1 #include<iostream>
2 #include<string>
3 using namespace std;
4 class Show
5 {
6 public:
7 void operator()(const string str) //“()”运算符重载函数
8 {
9 cout<<str<<endl;
10 }
11 float operator()(const float num) //“()”运算符重载函数
12 {
13 return num*num;
14 }
15 };
16 int main()
17 {
18 Show s;
19 s("abcdef");
20 cout<<s(4)<<endl;
21 return 0;
22 }
例3-10运行结果如图3-10所示。

图3-10 例3-10运行结果
在例3-10中,第7~10行代码定义了“()”运算符重载函数,用于输出字符串。第11~14行代码定义了另一个“()”运算符重载函数,返回计算后的float类型数据的平方。第18行代码创建了Show类对象s。第19~20行代码分别向对象s传入一个字符串和一个数据4,像调用函数一样调用对象s。由图3-10可知,程序成功输出了字符串和数据4的平方。
除此之外,仿函数还可以实现类中信息的传递。对例3-10代码进行修改,如果一个数的平方是偶数,则将私有成员变量_flag置为true,否则置为false。示例代码如下:
class Show
{
public:
Show(bool flag=false):_flag(flag){}
bool operator()(const int num)
{
int n=num*num;
if(n%2==0)
return true;
else
return false;
}
void dis()
{
cout<<_flag<<endl;
}
private:
bool _flag;
};
创建对象后,通过对象传入参数,判断仿函数的运算结果是偶数还是奇数,从而改变Show类中的成员变量_flag的值。
C++没有垃圾回收机制,堆内存资源的使用和释放需要自己编写程序实现,编写大型的程序可能会忘记释放内存,导致内存泄漏。为了解决这个问题,C++标准提出了智能指针机制。智能指针的本质是使用引用计数的方式解决悬空指针的问题,通过重载“*”和“−>”运算符来实现。
在学习引用计数、重载“*”和“−>”运算符之前,需要理解普通指针在资源访问中导致的指针悬空问题。下面通过案例演示悬空指针问题,如例3-11所示。
例3-11 refCount.cpp
1 #include<iostream>
2 #include<string>
3 using namespace std;
4 class Data
5 {
6 public:
7 Data(string str):_str(str)
8 {
9 cout<<"Data类构造函数"<<endl;
10 }
11 ~Data()
12 {
13 cout<<"Data类析构函数"<<endl;
14 }
15 void dis()
16 {
17 cout<<_str<<endl;
18 }
19 private:
20 string _str;
21 };17 cout<<_str<<endl;18}
22 int main()
23 {
24 Data *pstr1=new Data("I Love China");
25 Data *pstr2=pstr1;
26 Data *pstr3=pstr1;
27 pstr1->dis();
28 delete pstr1;
29 pstr2->dis();
30 return 0;
31 }
运行例3-11,编译器会抛出异常,如图3-11所示。
在例3-11中,Data类用于存储信息,第24行代码为Data类创建了一个位于堆内存的对象,并使pstr1指针指向该对象。第25行代码创建指针pstr2指向pstr1指向的空间。第26行代码创建指针pstr3指向pstr1指向的空间。指针pstr1、pstr2、pstr3共享同一个对象,若释放pstr1指向的对象,pstr2和pstr3仍然在使用该对象,将造成pstr2和pstr3无法访问资源,成为悬空指针,程序运行时出现异常。悬空指针如图3-12所示。

图3-11 例3-11程序异常

图3-12 悬空指针
为了解决悬空指针的问题,C++语言引入了引用计数的概念。引用计数是计算机科学中的一种编程技术,用于存储计算机资源的引用、指针或者句柄的数量。当引用计数为零时自动释放资源,使用引用计数可以跟踪堆中对象的分配和自动释放堆内存资源。
下面通过案例演示使用引用计数解决悬空指针的问题,通过重载“*”和“−>”运算符实现内存的自动管理,如例3-12所示。
例3-12 countSmart.cpp
1 #include<iostream>
2 #include<string>
3 using namespace std;
4 class Data{/*...*/}; //Data类定义在例3-10中
5 class Count //Count类用于存储指向同一资源的指针数量
6 {
7 public:
8 friend class SmartPtr;
9 Count(Data *pdata):_pdata(pdata),_count(1)
10 {
11 cout<<"Count类构造函数"<<endl;
12 }
13 ~Count()
14 {
15 cout<<"Count类析构函数"<<endl;
16 delete _pdata;
17 }
18 private:
19 Data *_pdata;
20 int _count;
21 };
22 //使用指针实现智能指针
23 class SmartPtr //SmartPtr类用于对指向Data类对象的指针实现智能管理
24 {
25 public:
26 SmartPtr(Data* pdata):_reNum(new Count(pdata))
27 {
28 cout<<"创建基类对象"<<endl;
29 }
30 SmartPtr(const SmartPtr&another):_reNum(another._reNum)
31 {
32 ++_reNum->_count;
33 cout<<"Smartptr类复制构造函数"<<endl;
34 }
35 ~SmartPtr()
36 {
37 if(--_reNum->_count==0)
38 {
39 delete _reNum;
40 cout<<"Smartptr类析构函数"<<endl;
41 }
42 }
43 Data *operator->()
44 {
45 return _reNum->_pdata;
46 }
47 Data &operator*()
48 {
49 return *_reNum->_pdata;
50 }
51 int disCount()
52 {
53 return _reNum->_count;
54 }
55 private:
56 Count *_reNum;
57 };
58 int main()
59 {
60 Data *pstr1=new Data("I Love China!");
61 SmartPtr pstr2=pstr1;
62 (*pstr1).dis();
63 SmartPtr pstr3=pstr2;
64 pstr2->dis();
65 cout<<"使用基类对象的指针数量:"<<pstr2.disCount()<<endl;
66 return 0;
67 }
例3-12运行结果如图3-13所示。
在例3-12中,第5~21行代码定义了Count类,类中的成员变量_pdata和_count为私有成员,并声明Sm artPtr类为友元类。Count类的目的是实现引用计数,封装了基类Data对象的指针,起到辅助作用。第23~57行代码定义了Sm artPtr类,用于实现智能指针,Sm artPtr类中的私有成员变量_reNum用于访问Count类的成员,其中第26~29行代码在创建Data类对象后,将Count类的指针_pdata指向存储于堆内存的Data类对象。第30~34行代码定义了复制构造函数,如果其他对象的指针使用Data数据,使计数_count加1。第35~42行代码定义析构函数释放Data类对象的资源,当记录指向Data类对象指针的数量_count为0时,释放资源。第43~46行代码重载运算符“−>”,返回指向Data类对象的指针。第47~50行代码重载运算符“*”,返回Data类对象。通过重载“*”和“−>”运算符就可以指针的方式实现Data类成员的访问。第60行代码申请堆内存储空间,存储Data类对象并初始化。

图3-13 例3-11运行结果
第61行代码定义了智能指针pstr2指向Data类对象。第62行代码通过重载“*”运算符访问Data类对象存储的数据。第63行代码定义了智能指针pstr3指向Data类对象。第64行代码通过重载“−>”运算符访问Data类对象存储的数据。引用计数的原理如图3-14所示。
由例3-12可知,在使用智能指针申请Data类对象存储空间后并没有使用delete释放内存空间。使用智能指针可以避免堆内存泄漏,只需申请,无须关注内存是否释放。通过重载“*”和“−>”运算符可以实现对象中成员的访问。

图3-14 引用计数的原理
C++11标准提供了unique_ptr、shared_ptr和weak_ptr三种智能指针(这三种类型的指针将在第10章详细讲解),高度封装的智能指针为编程人员带来了便利,也使得C++更加完善。
本章主要讲解了C++语言中的运算符重载,包括运算符重载的意义、语法和规则,以及常用的几种运算符的重载,如自增运算符、赋值运算符的重载等。运算符重载是C++语言重要的特性之一,读者学好本章内容会加深对面向对象中多态性的理解。
一、填空题
1. 双目运算符重载为类的成员函数,其左操作数为___。
2. 运算符重载仍然保持其原来的___、___、操作数和语法结构。3.C++中的运算符可以重载为类的___和___。
4. 当双目运算符重载为类的成员函数时,运算符的操作数是___。
二、判断题
1. 输入/输出运算符只能重载为类的友元函数。( )
2. 重载运算符不能改变原有运算符的语义。( )
3. 转换构造函数可以将一个标准类型数据转换为类对象。( )
4. 类型转换函数只能重载为类的成员函数。( )
三、选择题
1. 下列运算符中,不能重载的是( )。
A.?:
B.+
C..
D.<=
2. 关于运算符重载的规则,下列说法正确的是( )(多选)。
A.运算符重载可以改变运算符操作数
B.运算符重载可以改变运算符优先级
C.运算符重载可以改变运算符结合性
D.运算符重载不可以改变运算符语法结构
3. 关于运算符重载,下列说法正确的是( )。
A.C++已有的运算符均可以重载
B.运算符重载函数的返回类型不能声明为基本数据类型
C.在类型转换函数的定义中不需要声明返回类型
D.可以通过运算符重载创建新的运算符
4. 重载前置运算符“++”,则++c(c为对象)相当于执行了函数( )。
A.c.operator++(c,0)
B.c.operator++()
C.operator++(c)
D.operator++(c,0)
四、简答题
1. 简述什么是运算符重载。
2. 简述运算符的重载规则。
五、编程题
1. 定义一个计数器类Counter,包含私有成员int n,重载运算符“+”,实现对象的相加。
2.C++语言中不会检查数组是否越界。设计类Border,通过重载运算符“[]”检查数组是否越界。