C++程序设计:运算符重载

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指针指出;右操作数则需通过运算符重载函数的参数列表进行传递。双目运算符重载后的调用格式如下所示:

1
左操作数.运算符重载函数(右操作数);

比如,在前面的示例中,重载了“+”运算符,当调用a1+a2时,其实就相当于函数调用a1.oprerator+(a2)。

如果是单目运算符重载为类的成员函数,需要确定重载的运算符是前置运算符还是后置运算符。如果是前置运算符,则它的操作数就是函数调用者,无需传递参数,其调用格式如下所示:

1
左操作数.运算符重载函数();

比如,重载单目运算符“++”,如果重载的是前置运算符“++”,则++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++中,可以重载的运算符,如下表所示。

img

二、常用运算符的重载

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;

// 用对象a初始化b,调用的是拷贝构造函数
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)
{
// 从B类类型到A类类型的转换
...
}
};

下面通过案例,演示转换构造函数的用法,代码如下。

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;

// Data类用于存储信息
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无法访问资源,出现指针悬空,程序运行时出现异常。指针悬空的示意图如下图所示。

img

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;

// Data类用于存储信息
class Data
{
private:
string _str;
public:
Data(string str):_str(str)
{
cout<<"Data类构造函数"<<endl;
}
~Data()
{
cout<<"Data类析构函数"<<endl;
}
void dis()
{
cout<<_str<<endl;
}
};

// Count类用于存储指向同一资源的指针的数量
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;
}
};

// SmartPtr类用于对指向Data类对象的指针实现智能管理
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类对象存储的数据。引用计数的原理如下图所示。

img

在使用智能指针申请Data类对象存储空间后并没有使用delete释放内存空间。使用智能指针可以避免堆内存泄漏,只需申请,无须关注内存是否释放。通过重载“*”和“−>”运算符可以实现对象中成员的访问。

C++11标准提供了unique_ptr、shared_ptr和weak_ptr三种智能指针,高度封装的智能指针为编程人员带来了便利,也使得C++更加完善。

六、参考

[《C++程序设计教程》](《C++程序设计教程(第2版)》电子书在线阅读-黑马程序员 编著-得到APP (dedao.cn))


C++程序设计:运算符重载
https://kuberxy.github.io/2024/09/15/C++程序设计3:运算符重载/
作者
Mr.x
发布于
2024年9月15日
许可协议