C++的一大特性就是重载,重载使得程序更加简洁高效。在C++中不只函数可以重载,运算符也可以重载,运算符重载主要是面向对象之间的。
一、运算符重载概述
运算符重载,是指在类中重新定义运算符,赋予运算符新功能,满足自定义数据类型的运算需要。比如,运算符“+”,在int类中可对数据进行加法运算,在String类中可以连接两个字符串;运算符“>>”和“<<”,在int类中可以对数据进行右移和左移运算,在输入、输出流类中可以实现输入和输出操作。
1.1、运算符重载的语法
在C++中,使用operator关键字定义运算符重载。运算符重载语法格式如下:
1 2 3 4 5 返回值类型 operator 运算符名称(参数列表) { ... }
运算符重载的返回值类型、参数列表可以是任意数据类型。除了函数名称中的operator关键字,运算符重载函数与普通函数没有区别。
下面通过案例,演示运算符“+”、“−”的重载,代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include <iostream> using namespace std;class A {private : int _x; int _y;public : A (int x=0 , int y=0 ):_x(x),_y(y){} void show () const ; A operator +(const A& a) const ; A operator -(const A& a) const ; };void A::show () const { cout<<"(_x,_y)=" <<"(" <<_x<<"," <<_y<<")" <<endl; } A A::operator +(const A& a) const { return A (_x+a._x,_y+a._y); } A A::operator -(const A& a) const { return A (_x-a._x,_y-a._y); }int main () { A a1 (1 ,2 ) ; A a2 (4 ,5 ) ; A a; cout<<"a1: " ; a1.show (); cout<<"a2: " ; a2.show (); a=a1+a2; cout<<"a: " ; a.show (); a=a1-a2; cout<<"a: " ; a.show (); return 0 ; }
上述代码,第18~21行代码重载了运算符“+”,第22~25行代码重载了运算符“−”。在m ain()函数中,第28~29行代码创建并初始化类A的对象a1和a2,第35行代码通过重载的运算符“+”实现对象a1、a2相加并将结果保存到对象a中,第38行代码通过重载的运算符“−”实现对象a1、a2相减并将结果保存到对象a中。
1.2、运算符重载的形式
在重载运算符时,一般有两种形式:重载为类的成员函数和重载为类的友元函数。下面分别对这两种形式进行详细讲解。
1.2.1、重载为类的成员函数
重载为类的成员元函数时,可以通过隐含的this指针访问函数调用者,因此运算符的操作数可通过调用者或参数列表进行传递。比如,前面的示例,在重载运算符“+”、“−”时,就是通过调用者和参数列表实现参数传递的。不同的运算符所需的操作数不同,因此在具体的语法格式上存在差异。
如果是双目运算符重载为类的成员函数,则它有两个操作数:左操作数是对象本身的数据,可由this指针指出;右操作数则需通过运算符重载函数的参数列表进行传递。双目运算符重载后的调用格式如下所示:
比如,在前面的示例中,重载了“+”运算符,当调用a1+a2时,其实就相当于函数调用a1.oprerator+(a2)。
如果是单目运算符重载为类的成员函数,需要确定重载的运算符是前置运算符还是后置运算符。如果是前置运算符,则它的操作数就是函数调用者,无需传递参数,其调用格式如下所示:
比如,重载单目运算符“++”,如果重载的是前置运算符“++”,则++a1的调用相当于调用函数a1.operator++()。如果重载的是后置运算符“++”,则运算符重载函数需要带一个整型参数,即“operator++(int)”,这里的参数int仅仅表示后置运算,用于和前置运算区分,并无其他意义。
下面通过案例演示,前置运算符“++”与后置运算符“++”的重载,示例如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <iostream> using namespace std;class A {private : int _x; int _y;public : A (int x=0 , int y=0 ):_x(x),_y(y){} void show () const ; A operator ++(); A operator ++(int ); };void A::show () const { cout<<"(_x,_y)=" <<"(" <<_x<<"," <<_y<<")" <<endl; } A A::operator ++() { ++_x; ++_y; return *this ; } A A::operator ++(int ) { A a=*this ; ++(*this ); return a; }int main () { A a1 (1 ,2 ) , a2 (3 ,4 ) ; (a1++).show (); (++a2).show (); return 0 ; }
上述代码,第11~12行代码分别在类A中声明前置“++”和后置“++”运算符重载函数。第18~23行代码在类外实现前置“++”运算符重载函数,在函数内部,类的成员变量进行自增运算,然后返回当前对象(即this指针所指向的对象)。第24~29行代码在类外实现后置“++”运算符重载函数,在函数内部,创建一个临时对象保存当前对象的值,然后再将当前对象自增,最后返回保存初始值的临时对象。第32~34行代码创建了两个对象a1、a2,a1调用后置“++”,a2调用前置“++”。
1.2.2、重载为类的友元函数
运算符重载为类的友元函数,需要在函数前加friend关键字,其语法格式如下所示:
1 2 3 4 5 friend 返回值类型 operator 运算符名称(参数列表) { ... }
重载为类的友元函数时,由于没有隐含的this指针,因此运算符的所有操作数都必须通过参数列表进行传递。
下面通过案例,演示将运算符“+”和“−”重载为类的友元函数,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <iostream> using namespace std;class A {private : int _x; int _y;public : A (int x=0 , int y=0 ):_x(x), _y(y){} void show () const ; friend A operator +(const A& a1, const A& a2); friend A operator -(const A& a1, const A& a2); };void A::show () const { cout<<"(_x,_y)=" <<"(" <<_x<<"," <<_y<<")" <<endl; } A operator +(const A& a1, const A& a2) { return A (a1._x+a2._x, a1._y+a2._y); } A operator -(const A& a1, const A& a2) { return A (a1._x-a2._x, a1._y-a2._y); }int main () { A a1 (1 ,2 ) ; A a2 (4 ,5 ) ; A a; cout<<"a1: " ; a1.show (); cout<<"a2: " ; a2.show (); a=a1+a2; cout<<"a: " ; a.show (); a=a1-a2; cout<<"a: " ; a.show (); return 0 ; }
上述代码,第11~12行代码将“+”和“−”运算符重载函数声明为类A的友元函数。将运算符重载函数声明为类的友元函数,与重载为类的成员函数的用法和规则是相同的。
1.3、运算符重载的规则
运算符重载,通常用于增强自定义数据类型的运算功能,使其具有更广泛的多态特征。运算符重载的规则具体如下。
只能重载C++中已有的运算符,且不能创建新的运算符。例如,一个数的幂运算,试图重载“**”为幂运算符,使用2**4表示2^4是不可行的。
要保持运算符原有语义,且要避免没有目的地使用运算符重载。例如,运算符“+”重载后,应该实现相加的功能,而不应该实现相减或者其他功能。
不能改变优先级和结合性,也不能改变操作数和语法结构。
并非所有C++运算符都可以重载。比如“::”、“.”、“.*”、“?:”、sizeof、typeid等都是不可重载的。
在C++中,可以重载的运算符,如下表所示。
二、常用运算符的重载
2.1、输入/输出运算符
C++的输入输出标准库提供了“>>”和“<<”运算符执行输入、输出操作,但标准库只定义了基本数据类型的输入、输出操作,若想直接对类对象进行输入、输出,则需要在类中重载这两个运算符。重载运算符“<<”和“>>”后,类对象可以和基本数据类型一样,直接执行输入、输出操作,不用再编写show()成员函数,使程序更简洁。
与其他运算符不同,输入、输出运算符只能重载成类的友元函数。“<<”和“>>”运算符重载的格式如下:
1 2 3 4 ostream& operator <<(ostream&, const 类对象引用); istream& operator >>(istream&, 类对象引用);
输出运算符“<<”重载函数的第一个参数是ostream对象引用,该对象引用不能使用const修饰,第二个参数是输出对象的const引用。输入运算符“>>”重载函数的第一个参数是istream对象引用,第二个参数是要向其中存入数据的对象,该对象不能使用const修饰。
下面通过案例,演示输入/输出运算符重载的用法,代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <iostream> using namespace std;class A {private : int _x; int _y;public : A (int x=0 , int y=0 ):_x(x),_y(y){} friend ostream& operator <<(ostream& os, const A& a); friend istream& operator >>(istream& is, A& a); }; ostream& operator <<(ostream& os, const A& a) { os<<"(" <<a._x<<"," <<a._y<<")" ; return os; } istream& operator >>(istream& is, A& a) { is>>a._x>>a._y; return is; }int main () { A a1 (1 ,2 ) ; cout<<"a1: " <<a1<<endl; cout<<"请重新为a1对象输入数据: " <<endl; cin>>a1; cout<<"重新输入后a1: " <<a1<<endl; return 0 ; }
上述代码,第13~17行代码重载了输出运算符“<<”,第18~22行代码重载了输入运算符“>>”。在main()函数中,第25行代码创建类A对象a1并初始化,第26行代码直接使用重载的输出运算符输出对象a1的值,第28行代码调用重载的输入运算符为a1对象重新赋值,第29行代码调用重载的输出运算符输出对象a1的值。
2.2、关系运算符
关系运算符重载函数的返回值类型一般定义为bool类型,即返回true或false。关系运算符常用于条件判断中,重载关系运算符保留了关系运算符的原有含义。
下面通过案例,演示关系运算符的重载,代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include <iostream> using namespace std;class Student {private : int _id; double _score;public : Student (int id, double score):_id(id), _score(score){} void dis () { cout<<"学号: " <<_id<<"," <<"成绩: " <<_score<<endl; } friend bool operator ==(const Student& st1, const Student& st2); friend bool operator !=(const Student& st1, const Student& st2); friend bool operator >(const Student& st1, const Student& st2); friend bool operator <(const Student& st1, const Student& st2); };bool operator ==(const Student& st1, const Student& st2) { return st1._score==st2._score; }bool operator !=(const Student& st1, const Student& st2) { return !(st1._score==st2._score); }bool operator >(const Student& st1, const Student& st2) { return st1._score>st2._score; }bool operator <(const Student& st1, const Student& st2) { return st1._score<st2._score; }int main () { Student st1 (1001 ,96 ) , st2 (1002 ,105 ) ; cout<<"比较两名学生的成绩: " <<endl; if (st1>st2) st1.dis (); else if (st1<st2) st2.dis (); else cout<<"两名学生的成绩相同" <<endl; return 0 ; }
上述代码,重载了四个典型的比较运算符,重载比较运算符后,可以直接比较对象的大小,而实际实现中只是比较了对象中的score数据。如果没有重载关系运算符,需要先通过一个公有函数访问获得score,然后再来比较score的大小。
重载关系运算符时,有以下几点使用技巧。
通常关系运算符都要成对地重载,例如重载了“>”运算符,就要重载“<”运算符,反之亦然。
通常情况下,“==”运算符具有传递性,例如a==b,b==c,则a==c成立。
可以把一个运算符的工作委托给另一个运算符,通过重载后的结果进行判断。例如,本例中重载“!=”运算符是在重载“==”运算符的基础上实现的。
2.3、赋值运算符
对于赋值运算符来说,如果不重载,编译器会自动提供一个赋值运算符。这个默认的赋值运算符和默认的拷贝构造函数一样,实现的是浅拷贝。若数据成员中有指针,则默认的赋值运算符不能满足要求,会出现重析构的现象,这时就需要重载赋值运算符,实现深拷贝。
下面通过案例,演示赋值运算符的重载,代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 #define _CRT_SECURE_NO_WARNINGS #include <string.h> #include <iostream> using namespace std;class Assign {public : char * name; char * url;public : Assign (const char * name, const char * url); Assign (const Assign& temp); ~Assign () { delete []name; delete []url; } Assign& operator =(Assign& temp); }; Assign::Assign (const char * name, const char * url) { this ->name=new char [strlen (name)+1 ]; this ->url=new char [strlen (url)+1 ]; if (name) strcpy (this ->name,name); if (url) strcpy (this ->url,url); } Assign::Assign (const Assign& temp) { this ->name=new char [strlen (temp.name)+1 ]; this ->url=new char [strlen (temp.url)+1 ]; if (name) strcpy (this ->name,temp.name); if (url) strcpy (this ->url,temp.url); } Assign& Assign::operator =(Assign& temp) { delete []name; delete []url; this ->name=new char [strlen (temp.name)+1 ]; this ->url=new char [strlen (temp.url)+1 ]; if (name) strcpy (this ->name,temp.name); if (url) strcpy (this ->url,temp.url); return *this ; }int main () { Assign a ("百度" ,"https://www.baidu.com/" ) ; cout<<"对象a: " <<a.name<<" " <<a.url<<endl; Assign b (a) ; cout<<"对象b: " <<b.name<<" " <<b.url<<endl; Assign c ("天猫" ,"https://www.tmall.com/" ) ; cout<<"对象c: " <<c.name<<" " <<c.url<<endl; b=c; cout<<"对象b: " <<b.name<<" " <<b.url<<endl; return 0 ; }
上述代码,类Assign中含有指针数据成员,第38~49行代码在类外实现赋值运算符“=”重载函数。由于对象b已经存在,name和url指针所指区域范围大小已经确定,要复制新内容进去,则区域过大或过小都不好,因此重载赋值运算符时,需要内部先释放name、url指针,根据要复制的内容大小再分配一块内存区域,然后将内容复制进去。在main()函数中,第58行代码通过重载赋值运算符完成对对象b的赋值。
2.4、下标运算符
在程序设计中,通常使用下标运算符“[]”访问数组或容器中的元素。为了在类中方便地使用“[]”运算符,可以在类中重载“[]”运算符。重载“[]”运算符有两个目的:
“对象[下标]”的形式类似于“数组[下标]”,更加符合用户的编写习惯。
可以对下标进行越界检查。
重载下标运算符“[]”的语法格式如下所示:
1 2 3 4 5 返回值类型 operator [](参数列表) { ... }
上述格式中,“[]”运算符重载函数有且只有一个整型参数,表示下标值。重载下标运算符时,一般把返回值指定为一个引用。
下面通过案例,演示重载下标运算符“[]”的用法,代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <string.h> using namespace std;class Tag {private : int size; char * buf;public : Tag (int n); Tag (const char * src); ~Tag () { delete []buf; } char & operator [](int n); void show () { for (int i=0 ;i<size;i++) cout<<buf[i]; cout<<endl; } }; Tag::Tag (int n) { size=n; buf=new char [size+1 ]; *(buf+size)='\0' ; } Tag::Tag (const char * src) { buf=new char [strlen (src)+1 ]; strcpy (buf,src); size=strlen (buf); }char & Tag::operator [](int n) { static char ch=0 ; if (n>size||n<0 ) { cout<<"越界" <<endl; return ch; } return *(buf+n); }int main () { Tag arr1 (20 ) ; for (int i=0 ;i<20 ;i++) arr1[i]=65 +i; arr1.show (); Tag arr2 ("Itcast!" ) ; cout<<arr2[6 ]<<endl; arr2[6 ]='A' ; arr2.show (); return 0 ; }
上述代码,第4~23行代码定义了一个字符数组类Tag;第36~46行代码重载了“[]”运算符。在main()函数中,第49行代码创建字符数组对象arr1,指定数组大小为20;第50~51行代码通过“[]”运算符给数组赋值;第53行代码创建字符数组对象arr2并初始化;第55行代码调用“[]”运算符重载函数,对指定索引位置的字符元素进行修改。
三、类型转换
通过强制类型转换操作符,可将基本数据类型的数据,转换所成需要的类型,例如static_cast<int>(3.14)
,这个表达式是将实型数据3.14转换成整型数据。对于自定义的类,可通过类型转换函数,来实现自定义类与基本数据类型之间的转换。
3.1、类型转换函数
类型转换函数,也称为类型转换运算符重载函数,其定义格式如下:
1 2 3 4 5 operator 数据类型名() { ... }
类型转换函数以operator关键字开头,这一点和运算符重载规律一致。从类型转换函数格式可以看出,在重载的数据类型名前不能指定返回值类型,返回值的类型由重载的数据类型名确定,且函数没有参数。由于类型转换函数的主体是本类对象,因此只能将类型转换函数重载为类的成员函数。
下面通过案例,演示类型转换函数的用法,代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <string.h> using namespace std;class Student {private : string _id; char * _name;public : Student (string id, const char * name):_id(id) { _name=new char [strlen (name)+1 ]; strcpy (_name, name); } operator char *() { return _name; } void show () { cout<<"ID: " <<_id<<"," <<"name: " <<_name<<endl; } };int main () { Student s1 ("1001" ,"小明" ) ; cout<<"s1: " ; s1.show (); char * ch=s1; cout<<ch<<endl; return 0 ; }
上述代码,第15~18行代码定义了类型转换函数,用于将Student类的对象转换为char*类型;第29行代码通过调用重载的char*类型转换函数,将对象s1成功转换为了char*类型。
3.2、转换构造函数
转换构造函数,是指构造函数只有一个参数,且是另一个类的const引用。转换构造函数,不仅可以将一个标准类型数据转换为类对象,还可以将另一个类的对象转换为转换构造函数所在的类对象。
转换构造函数的语法格式如下所示:
1 2 3 4 5 6 7 8 class A { A (const B& b) { ... } };
下面通过案例,演示转换构造函数的用法,代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #include <iostream> using namespace std;class Solid {private : int _x,_y,_z;public : Solid (int x, int y, int z):_x(x),_y(y),_z(z){} void show () { cout<<"三维坐标: " <<_x<<"," <<_y<<"," <<_z<<endl; } friend class Point ; };class Point {private : int _x,_y;public : Point (int x, int y):_x(x),_y(y){} Point (const Solid &another) { this ->_x=another._x; this ->_y=another._y; } void show () { cout<<"平面坐标: " <<_x<<"," <<_y<<endl; } };int main () { cout<<"原始坐标" <<endl; Point p (1 ,2 ) ; p.show (); Solid s (3 ,4 ,5 ) ; s.show (); cout<<"三维坐标转换平面坐标" <<endl; p=s; p.show (); return 0 ; }
上述代码,第3~14行代码定义了表示三维坐标点的类Solid;第15~30行代码定义了表示平面坐标点的类Point,在Point类中定义了一个转换构造函数,将三维坐标点Solid类对象转换为平面坐标点Point类的数据。
需要注意的是,由于在Point类中要访问Solid的成员变量,因此Solid类声明Point类为友元类。
四、仿函数——重载“()”运算符
仿函数,是指在类中重载“()”运算符后,这个类的对象可以像函数一样使用。仿函数在STL的算法中使用比较广泛。此外,lambda表达式在实现过程中也使用了仿函数。
下面通过案例,演示重载“()”运算符的用法,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <iostream> using namespace std;class Show {public : void operator () (const string str) { cout<<str<<endl; } float operator () (const float num) { return num*num; } };int main () { Show s; s ("abcdef" ); cout<<s (4 )<<endl; return 0 ; }
上述代码,第7~10行代码定义了“()”运算符重载函数,用于输出字符串。第11~14行代码定义了另一个“()”运算符重载函数,返回计算后的float类型数据的平方。第18行代码创建了Show类对象s。第19~20行代码分别向对象s传入一个字符串和一个数据4,像调用函数一样调用对象s。
除此之外,仿函数还可以实现类中信息的传递。对上述代码进行修改,如果一个数的平方是偶数,则将私有成员变量_flag置为true,否则置为false。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <iostream> using namespace std;class Show {private : bool _flag;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; } };int main () { Show s; s.dis (); s (3 ); s.dis (); s (4 ); s.dis (); return 0 ; }
创建对象后,通过对象传入参数,判断仿函数的运算结果是偶数还是奇数,从而改变Show类中的成员变量_flag的值。
五、智能指针——重载“*”和“->”运算符
C++没有垃圾回收机制,堆内存资源的使用和释放需要自己编写程序实现。在编写大型程序时,可能会忘记释放内存,导致内存泄漏。为了解决这个问题,C++标准提出了智能指针机制。
5.1、指针悬空问题
理解智能指针,需要先理解普通指针在资源访问中导致的指针悬空问题。下面通过案例,演示指针悬空问题,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <iostream> using namespace std;class Data {private : string _str;public : Data (string str):_str(str) { cout<<"Data类构造函数" <<endl; } ~Data () { cout<<"Data类析构函数" <<endl; } void dis () { cout<<_str<<endl; } };int main () { Data *pstr1=new Data ("I Love China" ); Data *pstr2=pstr1; Data *pstr3=pstr1; pstr1->dis (); delete pstr1; pstr2->dis (); return 0 ; }
上述代码,第24行代码为Data类创建了一个位于堆内存的对象,并使pstr1指针指向该对象。第25行代码创建指针pstr2指向pstr1指向的空间。第26行代码创建指针pstr3指向pstr1指向的空间。指针pstr1、pstr2、pstr3共享同一个对象,若释放pstr1指向的对象,pstr2和pstr3仍然在使用该对象,将造成pstr2和pstr3无法访问资源,出现指针悬空,程序运行时出现异常。指针悬空的示意图如下图所示。
5.2、智能指针
智能指针的本质是,使用引用计数的方式解决指针悬空问题,它是通过重载“*”和“−>”运算符实现的。
引用计数是计算机科学中的一种编程技术,用于存储计算机资源的引用、指针或者句柄的数量。使用引用计数可以跟踪堆中对象的分配和自动释放堆内存资源。
下面通过案例,演示使用引用计数解决指针悬空问题,代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include <iostream> using namespace std;class Data {private : string _str;public : Data (string str):_str(str) { cout<<"Data类构造函数" <<endl; } ~Data () { cout<<"Data类析构函数" <<endl; } void dis () { cout<<_str<<endl; } };class Count {private : Data *_pdata; int _count;public : friend class SmartPtr ; Count (Data *pdata):_pdata(pdata),_count(1 ) { cout<<"Count类构造函数" <<endl; } ~Count () { cout<<"Count类析构函数" <<endl; delete _pdata; } };class SmartPtr {private : Count *_reNum;public : SmartPtr (Data *pdata):_reNum(new Count (pdata)) { cout<<"创建基类对象" <<endl; } SmartPtr (const SmartPtr& another):_reNum(another._reNum) { ++_reNum->_count; cout<<"SmartPtr类复制构造函数" <<endl; } ~SmartPtr () { if (--_reNum->_count==0 ) { delete _reNum; cout<<"SmartPtr类析构函数" <<endl; } } Data* operator ->() { return _reNum->_pdata; } Data& operator *() { return *_reNum->_pdata; } int disCount () { return _reNum->_count; } };int main () { Data *pstr1=new Data ("I Love China!" ); SmartPtr pstr2=pstr1; (*pstr1).dis (); SmartPtr pstr3=pstr2; pstr2->dis (); cout<<"使用基类对象的指针数量: " <<pstr2.disCount ()<<endl; return 0 ; }
上述代码,第5~21行代码定义了Count类,类中的成员变量_pdata和_count为私有成员,并声明SmartPtr类为友元类。Count类的目的是实现引用计数,封装了基类Data对象的指针,起到辅助作用。第23~57行代码定义了SmartPtr类,用于实现智能指针,SmartPtr类中的私有成员变量_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类对象并初始化。第61行代码定义了智能指针pstr2指向Data类对象。第62行代码通过重载“*”运算符访问Data类对象存储的数据。第63行代码定义了智能指针pstr3指向Data类对象。第64行代码通过重载“−>”运算符访问Data类对象存储的数据。引用计数的原理如下图所示。
在使用智能指针申请Data类对象存储空间后并没有使用delete释放内存空间。使用智能指针可以避免堆内存泄漏,只需申请,无须关注内存是否释放。通过重载“*”和“−>”运算符可以实现对象中成员的访问。
C++11标准提供了unique_ptr、shared_ptr和weak_ptr三种智能指针,高度封装的智能指针为编程人员带来了便利,也使得C++更加完善。
六、参考
[《C++程序设计教程》](《C++程序设计教程(第2版)》电子书在线阅读-黑马程序员 编著-得到APP (dedao.cn) )