文章目录💬
- 一.运算符重载基础知识
- ①基本概念
- ②运算符重载的规则
- ③运算符重载形式
- ④运算符重载建议
- 二.常用运算符重载
- ①左移(<<)和右移(>>)运算符重载
- 1️⃣重载后函数参数是什么?
- 2️⃣重载的函数返回类型是什么?
- 3️⃣重载为哪种函数?
- 4️⃣代码实现
- ②赋值(=)运算符重载
- 知识卡片:
- ③关系运算符(== != > <)重载
- ④前置和后置(++/- -)重载
- 思考🤔
- ⑤类型转换(类型转换函数和转换构造函数)
- 1️⃣类型转换函数
- 2️⃣转换构造函数
- ⑥数组下标([])运算符重载
- ⑦指针运算符(*、 ->)重载
- 智能指针类
- ⑧函数调用()运算符重载——仿函数
一.运算符重载基础知识
C++的一大特性就是重载,重载使得程序更加简洁高效。在C++中不只函数可以重载,运算符也可以重载,运算符重载主要是面向对象之间的。
①基本概念
运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
//对于基础数据类型,运算符可以有很好的效果
int a=20,b=20;
int c=a+b;
//对于自定义数据类型类创建的对象运算,是十分繁琐的
//例如:
class Maker
{
public:
//有参构造函数
Maker(int id, int age)
{
this->id = id;
this->age = age;
}
public:
int id;
int age;
};
void test()
{
Maker m1(1,19);
Maker m2(2,20);
Maker m3(3,17);
m3=m1+m2;//err
//如果我们想要m1和m2相加赋值给m3,我们可以这样写
//m3.id=m1.id+m2.id;
//m3.age=m1.age+m2.age;
//这样写太繁琐,如果对象内部有很多成员变量,而且还是私有怎么办?
//C++用运算符重载来解决问题
}
运算符重载语法:
在C++中,使用operator关键字定义运算符重载。运算符重载语法格式如下:
函数的参数列表中参数个数取决于两个因素。
运算符是单目(一个参数)的双目(两个参数);
❤️运算符被定义为全局函数,对于单目运算符是一个参数,对于双目运算符是两个参数。
❤️被定义为类的成员函数,对于单目运算符没有参数,对于双目运算符是一个参数(因为类本身为左侧参数(this))。
这种定义很像一个普通的函数定义,只是函数的名字由关键字operator及其紧跟的运算符组成。差别仅此而已。它像任何其他函数一样也是一个函数,当编译器遇到适当的模式时,就会调用这个函数。
通过案例两个对象相加演示“+”运算符的重载:
class Maker
{
public:
Maker(int id, int age)
{
this->id = id;
this->age = age;
}
public:
int id;
int age;
};
Maker operator+(Maker &p1,Maker &p2)//2.编译器检查参数是否对应,第一个参数是加号的左边,第二参数是加号的右边
{
Maker temp(p1.id + p2.id, p1.age + p2.age);
return temp;
}
void test()
{
Maker m1(1, 20);
Maker m2(2, 22);
Maker m3=m1 + m2;//1.编译器看到两个对象相加,那么编译器会去找有没有叫operator+的函数
cout << "id:" << m3.id << " age:" << m3.age << endl;
//多个对象相加
Maker m4 = m1 + m2 + m3;
cout << "id:" << m4.id << " age:" << m4.age << endl;
}
通过上面案例可以知道,重载运算符并没有改变其原来的功能,只是增加了针对自定义数据类型的运算功能,具有了更广泛的多态特征。
②运算符重载的规则
- 只能重载C++中已有的运算符,且不能创建新的运算符。例如,一个数的幂运算,试图重载“* *”为幂运算符,使用2 ** 4表示2^4是不可行的。
- 重载后运算符不能改变优先级和结合性,也不能改变操作数和语法结构。
- 运算符重载的目的是针对实际运算数据类型的需要,重载要保持原有运算符的语义,且要避免没有目的地使用运算符重载。例如,运算符“+”重载后实现相加的功能,而不会重载“+”为相减或者其他功能。
- 并非所有C++运算符都可以重载,可以重载的运算符如下图所示。其他运算符是不可以重载的,如“::”、“.”、“.*”、“?:”、sizeof、typeid等。
③运算符重载形式
运算符重载一般有三种形式:
- 重载为普通全局函数(不推荐,因为重载运算符为普通函数,只能访问类的公有成员)
- 重载为类的成员函数
- 将重载后的全局函数声明为类的友元函数
④运算符重载建议
- ✅下面的运算符只能通过类的成员函数进行重载。
=:赋值运算符
[]:下标运算符
() :函数调用运算符
->:通过指针访问类成员的运算符。
- ✅<< 和 >> 操作符最好通过类的友元函数进行重载
- ✅不要重载 && 和 || 操作符,因为无法实现短路规则
//短路规则
a&b&c只要a为假,整个式子就是假,b往后就短路了,不需要看
a|b|c只要a为真,整个式子就是真,b往后就短路了,不需要看
常规建议:
拓展:
二.常用运算符重载
①左移(<<)和右移(>>)运算符重载
C++输入输出标准库提供了“>>”和“<<”运算符执行输入、输出操作,但标准库只定义了基本数据类型的输入、输出操作,若要直接对类对象进行输入、输出,则需要在类中重载这两个运算符。
与其他运算符不同的是,输入、输出运算符只能重载成类的友元函数。“<<”和“>>”运算符重载的格式如下:
解释:
1️⃣重载后函数参数是什么?
①<<和>>是双目运算符,重载时函数需要两个参数,那么参数的类型是什么?要打印的对象类型显而易见,但cout和cin这两个对象的类型是什么呢?
cout 是 ostream 类的对象。cin是istream类的对象。ostream 、istream类、 cout 和cin都是在头文件 < iostream > 中声明的。
⚠️注意:重载的函数的参数都需要用引用,cout是ostream类的对象,cin是istream类的对象,ostream类和istream中的拷贝构造函数是私有的无法调用,如果不加引用,传参时会调用ostream类和istream类中的拷贝构造,编译会出现冲突。
2️⃣重载的函数返回类型是什么?
以cout为例,cin同理,重载的函数返回类型可以是void,但有缺点,一次只能打印一个对象,不能连续打印。
cout<<m1;✔️
cout<<m1<<m2<<m3;✖️等价于void<<m2<<m3
cout<<m1<<endl;✖️
解决方法就是返回对象为ostream &,要加引用(&),不加引用就调用ostream的拷贝构造函数,但ostream类的拷贝构造函数是私有的。
cout<<m1<<m2<<m3;等价于cout<<m2<<m3~cout<<m3;
3️⃣重载为哪种函数?
考虑到cout对象在<<运算符左边, 如果使用类的成员函数重载<<的话, 需要在ostream类中重载函数,因为ostream是标准库定义的类,我们不能更改库中代码,在左边不符合要求。
普通函数不能访问类的私有变量,直接pass掉。
所以我们选择将重载后的全局函数声明为类的友元函数。
4️⃣代码实现
class Maker
{
//声明重载运算符的函数
friend ostream& operator<<(ostream &out,Maker &m1);
friend istream& operator>>(istream &in,Maker &m1);
public:
int id;
string Name;
Maker(int a,int b,int c,string name)
{
this->id=a;
this->age=b;
this->money=c;
this->Name=name;
}
private:
int age;
int money;
};
ostream& operator<<(ostream &out,Maker &m1)
{
cout<<m1.id<<'|'<<m1.age<<'|'<<m1.money<<'|'<<m1.Name<<endl;
return out;
}
istream& operator>>(istream &in,Maker &m1)
{
in>>m1.id>>m1.age>>m1.money>>m1.Name;
return in;
}
void test()
{
Maker m1(100,18,5999,"强风吹拂");
Maker m2(100,18,7999,"强风吹拂king");
cout<<m1<<m2;
cout<<"请重新为m1和m2对象输入数据:"<<endl;
cin>>m1>>m2;
cout<<m1<<m2;
cout<<1000<<100.3142<<endl;
//重载后基础类型,照样可以输入输出
//因为左移和右移运算符基础类型本没有输入输出的能力
//之所以有是因为左移和右移运算符输入输出基本数据在类内已经全部重载一遍了
}
②赋值(=)运算符重载
赋值符常常初学者的混淆。这是毫无疑问的,因为’=’在编程中是最基本的运算符,可以进行赋值操作,也能引起拷贝构造函数的调用。
知识卡片:
1.编译器默认给类提供了一个默认的赋值运算符重载函数,默认的赋值运算符重载函数对成员变量进行了简单的赋值操作。
🔯例如:
class Maker
{
public:
Maker()
{
id = 0;
age = 0;
}
Maker(int id, int age)
{
this->id = id;
this->age = age;
}
public:
int id;
int age;
};
void test()
{
Maker m1(10, 20);
Maker m2;
m2 = m1;//赋值操作
//默认的赋值运算符重载函数进行了简单的赋值操作
cout << m2.id << " " << m2.age << endl;
}
2.区分赋值操作和拷贝构造函数,因为’=’在编程中是最基本的运算符,可以进行赋值操作,也能引起拷贝构造函数的调用。
区分的关键在于看对象有没有创建。
🔯例如:
class Student
{
public:
Student(int id, int age)
{
this->ID = id;
this->Age = age;
}
void Printf()
{
cout << this->ID << this->Age << endl;
}
private:
int ID;
int Age;
};
void test()
{
Student s1(10, 20);
//调用拷贝构造
Student s2 = s1;
s1.Printf();
s2.Printf();
//如果一个对象还没有被创建,则必须初始化,也就是调用构造函数
//上述例子由于s2还没有初始化,所以会调用构造函数
//由于s2是从已有的s1来创建的,所以只有一个选择
//就是调用拷贝构造函数
Student s3(101, 100);
//这是赋值操作
s3 = s1;
s3.Printf();
//由于s3已经创建,不需要再调用构造函数,这时候调用的是重载的赋值运算符
}
3.当类的成员变量有指针时,有可能会出现两个问题
一是在创建对象调用拷贝构造函数时,会出现浅拷贝问题,即一个对象的成员变量(指针)被拷贝给另一个对象的成员变量,然后两个对象的成员变量(指针)指向同一份空间,在析构函数调用时就会出现同一块空间释放2次。
解决方法深拷贝。
二是在赋值时,两个对象都已被创建,此时赋值会导致一个对象的成员变量(指针)会被覆盖,导致内存泄露。
解决方法重载=运算符。
重载=运算符解决上述的第二个问题:
class Student
{
public:
Student(const char *name)
{
pName = new char[strlen(name) + 1];
strcpy(pName, name);
}
//防止浅拷贝
Student(const Student &stu)
{
pName = new char[strlen(stu.pName) + 1];
strcpy(pName, stu.pName);
}
//重写赋值运算符重载函数
Student &operator=(const Student &stu)
{
//1.不能确定this->pName指向的空间是否能装下stu中的数据,所以先释放this->pName指向的空间
if (this->pName != NULL)
{
delete[] this->pName;
this->pName = NULL;
}
//2.申请堆区空间,大小由stu决定
this->pName = new char[strlen(stu.pName) + 1];
//3.拷贝数据
strcpy(this->pName, stu.pName);
//4.返回对象本身
return *this;
}
~Student()
{
if (pName != NULL)
{
delete[] pName;
pName = NULL;
}
}
void printStudent()
{
cout << "Name:" << pName << endl;
}
public:
char *pName;
};
void test()
{
Student s1("强风吹拂");
Student s2("强风吹拂king");
s1.printStudent();
s2.printStudent();
s1 = s2;//赋值操作
s1.printStudent();
s2.printStudent();
}
4.为什么operator=返回一个reference to *this ?
①为了实现连续赋值,赋值操作符必须返回一个引用指向操作符的左侧实参。这是你为class实现赋值操作符必须遵循的协议。这个协议不仅适用于标准的赋值形式,也适用于+=、-=、*=等等。
②如果不加引用&,对象以值的形式返回,会在返回处调用拷贝构造函数,产生一个新对象。
🔯例如:
void test()
{
Student s1("a");
Student s2("b");
Student s3("c");
s1 = s2 = s3;//s3赋值s2,s2赋值给s1
//判断s3赋值给s2返回的是不是s2这个对象。
cout << &(s2 = s3) << endl;
cout << &s2 << endl;
}
- 一方面如果operator+返回类型不带引用,那么在返回的过程中表达式会调用拷贝构造函数,产生一个新的对象,就不是s2这个对象了。
- 另外一个方面:s1=s2=s3,赋值运算符本来的寓意,是s3赋值s2,s2赋值给s1,也就是说s2=s3这个表达式要返回s2这个对象,所以要返回引用。此处放对比图
③关系运算符(== != > <)重载
关系运算符(如“==”或“<”)也可以重载,关系运算符的重载函数返回值类型一般定义为bool类型,即返回true或false。关系运算符常用于条件判断中,重载关系运算符保留了关系运算符的原有含义。
🃏案例演示关系运算符重载:
class Student
{
private:
string _id;
double _score;
public:
Student(string 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;//重载“<”运算符
}
void test()
{
Student st1("22031202000", 101), st2("22031202001", 148);
cout << "比较两名学生的成绩:" << endl;
if (st1>st2)
st1.dis();
else if (st1<st2)
st2.dis();
else
cout << "两名学生成绩相同:" << endl;
}
关系运算符重载有以下几点使用技巧:
- 通常关系运算符都要成对地重载,例如重载了“>”运算符,就要重载“<”运算符,反之亦然。
- 通常情况下,“==”运算符具有传递性,例如:a= =b,b= =c,则a= =c成立。
- 可以把一个运算符的工作委托给另一个运算符,通过重载后的结果进行判断。例如,本例中重载“!=”运算符是在重载“==”运算符的基础上实现的。
④前置和后置(++/- -)重载
重载的++和- -运算符有点让人不知所措,因为前置和后置的运算符一样,都是++或- -,重载后的函数名都是operator++(- -),不便于区分,所以C++给我们提供了解决方法——占位参数。
即允许写一个增加了无用 int 类型形参的版本,编译器处理++或–前置的表达式时,调用参数个数正常的重载函数;处理后置表达式时,调用多出一个参数的重载函数。
重载形式:
例如:
class Maker
{
friend ostream &operator<<(ostream &out, Maker &m);
public:
Maker(int a)
{
this->a = a;
}
//重载前置加加,重载后的函数参数个数正常0个
Maker &operator++()
{//返回类型是你自定义的类型,因为有++(m++)情况存在
++this->a;//先完成递增操作
return *this;//再返回当前对象
}
//重载后置加加,重载后的函数参数比正常情况多一个占位参数int
Maker operator++(int)//占位参数,必须是int
{
//后置加加,先返回,在加加,
//但一旦先返回,加加就操作不了,所以我们采取一个临时对象来延迟返回操作。
//先保存旧值,然后加加,最后返回旧值
Maker tmp(*this);//1.*this里面的值a是等于2
++this->a;//这个对象的a等3
return tmp;
}
private:
int a;
};
ostream &operator<<(ostream &out, Maker &m)
{
out << m.a << endl;
return out;
}
void test()
{
Maker m1(1);
cout << m1 << endl;//1
cout << ++m1 << endl;//2
//++(++m1);
cout << m1++ << endl;//2 这里返回的拷贝的tmp对象
cout << m1 << endl;//3 这里打印的是++this->a的对象
//同等条件下,优先使用前置加加,不需要产生新的对象和调用拷贝构造
}
对比前置++和后置++运算符的重载可以发现,后置++运算符的执行效率比前置的低。因为后置方式的重载函数中要多生成一个局部对象 tmp,而对象的生成会引发构造函数调用,需要耗费时间。同理,后置–运算符的执行效率也比前置的低。
思考🤔
为什么前置++运算符的返回值类型是Maker &,而后置++运算符的返回值类型是Maker?(Maker是你自定义的数据类型)
原因有二个方面:
- ①是因为运算符重载最好保持原运算符的用法。C++ 固有的前置++运算符的返回值本来就是操作数的引用,而后置++运算符的返回值则是操作数值修改前的复制品。
- ②后置++或- -,返回的是局部对象(默认情况下,局部对象的声明周期局限于所在函数的每次执行期间,只有当函数被调用的时候才存在),当函数执行完毕时,会立即释放分配给局部对象的存储空间。此时,对局部对象的引用就会指向不确定的内存。
⑤类型转换(类型转换函数和转换构造函数)
基本数据类型的数据可以通过强制类型转换操作符将数据转换成需要的类型,例如static_cast(3.14),这个表达式是将实型数据3.14转换成整型数据。对于自定义的类,C++提供了类型转换函数来实现自定义类与基本数据类型之间的转换。
1️⃣类型转换函数
对于自定义的类,C++提供了类型转换函数用来将类对象转换为基本数据类型。
类型转换函数也称为类型转换运算符重载函数,定义格式如下所示:
类型转换函数以operator关键字开头,这一点和运算符重载规律一致。从类型转换函数格式可以看出,在重载的数据类型名前不能指定返回值类型,返回值的类型由重载的数据类型名确定,且函数没有参数。由于类型转换函数的主体是本类的对象,因此只能将类型转换函数重载为类的成员函数。
✅示例:
class king
{
public:
king(string id,const char*name)
{
this->ID=id;
Name = new char[strlen(name) + 1];
strcpy(Name, name);
}
void Show()
{
cout<<"ID:"<<ID<<","<<"Name:"<<Name<<endl;
}
operator char*()//类型转换运算符重载
{//如果要将对象转换为char*指针
//就直接将对象转换为自己的成员char*类型的Name
return Name;
}
private:
string ID;
char *Name;
};
void test()
{
king k1("2203120000","强风吹拂king");
k1.Show();
char *PK=k1;//调用类型转换函数
cout<<PK<<endl;
//通过调用重载的char*类型转换函数,将对象s1成功转换为了char*类型
}
将对象k1成功转换为char*类型
2️⃣转换构造函数
转换构造函数指的是构造函数只有一个参数,且参数不是本类的const引用。用转换构造函数不仅可以将一个标准类型数据转换为类对象,也可以将另一个类的对象转换为转换构造函数所在的类对象。转换构造函数的语法格式如下所示:
🃏案例演示转换构造函数:
三维坐标类转换为二维坐标类。
class Solid//三维坐标类
{
friend class Point;
//由于需要在Point类中访问Solid的成员变量
//因此将Solid类声明为Point类的友元类。
public:
Solid(int x,int y,int z) :x(x), y(y),z(z){}
void Show()
{
cout<<"三维坐标"<<"("<<x<<","<<y<<","<<z<<")"<<endl;
}
private:
int x,y,z;
};
class Point//二维点类
{
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;
}
private:
int x,y;
};
void test()
{
Point p1(1,1);
p1.Show();
Solid s1(2,2,2);
s1.Show();
cout<<"三维转换二维坐标"<<endl;
p1=s1;
p1.Show();
}
⑥数组下标([])运算符重载
在程序设计中,通常使用下标运算符“[]”访问数组或容器中的元素。为了在类中方便地使用“[]”运算符,可以在类中重载运算符“[]”。重载“[]”运算符有两个目的:
- “对象[下标]”的形式类似于“数组[下标]”,更加符合用户的编写习惯。
- 可以对下标进行越界检查。
重载下标运算符“[]”的语法格式如下所示:
上述格式中,“[]”运算符重载函数有且只有一个整型参数,表示下标值。重载下标运算符时一般把返回值指定为一个引用,因为数组下标既要做到读数据,也要做到写数据,所以要能当左右值。
✅案例演示:
class MyArray
{
public:
MyArray();//无参构造函数
MyArray(const MyArray &arr);//拷贝构造函数
MyArray(int capacity, int val = 0);//有参构造参数,如果不指名初始值,默认为val=0。
~MyArray();//析构函数
//重写赋值运算符重载函数
MyArray&operator=(const MyArray &m);
//重写[]运算符,要能当左右值,左值可以放数据,右值可以读数据
int &operator[](int index);
//打印数据
void PrintfMyArray();
private:
int *pArray;//指向堆区空间,存储数据
int mSize;//元素个数
int mCapacity;//数组容量
};
//无参构造函数
MyArray::MyArray()
{
this->mCapacity = 10;
this->mSize = 0;
this->pArray = new int[this->mCapacity];
for (int i = 0; i < this->mCapacity; i++)
{
this->pArray[i] = 0;
}
}
//析构函数
MyArray::~MyArray()
{
if (this->pArray != NULL)
{
delete[] this->pArray;
this->pArray = NULL;
}
}
//拷贝构造函数
MyArray::MyArray(const MyArray &arr)
{
this->mCapacity = arr.mCapacity;
this->mSize = arr.mSize;
//1.申请空间
this->pArray = new int[arr.mCapacity];
//2.拷贝数据
for (int i = 0; i < this->mSize; i++)
{
this->pArray[i] = arr.pArray[i];
}
}
//有参构造函数,不指定初始值,则默认全部初始化为0
MyArray::MyArray(int capacity, int val)
{
this->mCapacity = capacity;
this->mSize = capacity;
this->pArray = new int[capacity];
for (int i = 0; i < this->mSize; i++)
{
this->pArray[i] = val;
}
}
//重写赋值运算符重载函数
MyArray&MyArray::operator=(const MyArray &m)
{
//1.释放原来的空间
if (this->pArray != NULL)
{
delete[] this->pArray;
this->pArray = NULL;
}
this->mCapacity = m.mCapacity;
this->mSize = m.mSize;
//2.申请空间,大小由m决定
this->pArray = new int[m.mCapacity];
//3.拷贝数据
for (int i = 0; i < this->mCapacity; i++)
{
this->pArray[i] = m.pArray[i];
}
return *this;
}
//要能当左右值
int &MyArray::operator[](int index)
{
static int x=-1;
//数组下标检查
if(mSize<index||index<0)
{
cout<<"index越界"<<endl;
return x;
}
else if (this->mSize ==index)
{ //当m.Size元素个数等于下标index
this->mSize++;
}
//当else if执行说明此时要加入新数据,此时m.Size先要++,然后return this->pArray[index];返回的是左值。
//上面两个if都不执行,说明只读取下标为index的元素,然后return this->pArray[index];返回的是右值。
return this->pArray[index];
}
void MyArray::PrintfMyArray()
{
for(int i=0;i<mSize;i++)
{
cout<<pArray[i]<<" |";//调用[]运算符重载函数,输出数组类
}
}
void test()
{
MyArray arr;//无参构造函数
for (int i = 0; i < 10; i++)
{
arr[i] = i + 10;//调用[]运算符重载函数,初始化数组类
}
arr.PrintfMyArray();
cout << endl;
MyArray arr2(10, 66);//有参构造函数
arr2.PrintfMyArray();
cout << endl;
MyArray arr3(arr2);//拷贝构造函数
arr3.PrintfMyArray();
cout << endl;
MyArray arr4;
arr4 = arr;//调用赋值运算符=的重载函数,arr赋值给arr4
arr4.PrintfMyArray();
cout << endl;
arr2[6]=10086;//调用[]运算符,修改特定下标的值
cout << arr2[6] << "|";
cout << endl;
cout << arr2[60] << "|";
cout << endl;
}
⑦指针运算符(*、 ->)重载
智能指针类
智能指针(smart pointer)是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露。
智能指针的本质是使用引用计数的方式解决悬空指针的问题,通过重载“*”和“?>”运算符来实现。
为什么有智能指针类?
在平时我们写代码的时候,用new开辟出来的空间虽然我们知道要进行资源的回收,但可能会因为程序执行流的改变导致资源没有归还所导致的内存泄漏的问题,智能指针就帮助我们解决这个问题。
在学习引用计数、重载“*”和“?>”运算符之前,需要理解普通指针在资源访问中导致的指针悬空问题。
📝例如:
class king
{
public:
king(string name) :Name(name)
{
cout << "king构造函数调用" << endl;
}
~king(){}
void Show()
{
cout << "强风吹拂king的博客" << endl;
}
private:
string Name;
};
void test()
{
king *pstr1 = new king("感谢支持我的博客");
king *pstr2 = pstr1;
king *pstr3 = pstr1;
pstr1->Show();
delete pstr1;
pstr2->Show();
}
❗️问题:
指针pstr1、pstr2、pstr3共享同一个对象,若释放pstr1指向的对象,pstr2和pstr3仍然在使用该对象,将造成pstr2和pstr3无法访问资源,成为悬空指针,程序运行时出现异常。
为了解决悬空指针的问题,C++语言引入了引用计数的概念。引用计数是计算机科学中的一种编程技术,用于存储计算机资源的引用、指针或者句柄的数量。当引用计数为零时自动释放资源,使用引用计数可以跟踪堆中对象的分配和自动释放堆内存资源。
案例演示:
class king
{
public:
king(string name) :Name(name)
{
cout << "king构造函数调用" << endl;
}
~king(){}
void Show()
{
cout << "感谢支持强风吹拂king的博客" << endl;
}
private:
string Name;
};
class Count//Count类用于存储指向同一资源的指针数量
{
//声明SmartPtr智能指针类为Count类的友元类
//因为SmartPtr要访问Count类成员
friend class SmartPtr;
public:
//Count成员pking指向由SmartPtr类传过来king类对象的指针
Count(king *pstr1) :pking(pstr1), count(1)
{
cout << "Count类构造函数" << endl;
}
~Count()
{
cout << "Count类析构函数" << endl;
delete pking;//释放king类指针
}
private:
king *pking;
int count;
};
class SmartPtr//SmartPtr类用于对指向king类对象的指针实现智能管理
{
public:
//CountNum是成员对象指针,指向由king类指针初始化的Count类
SmartPtr(king* pstr1) : CountNum(new Count(pstr1))
{
cout << "SmartPtr有参构造函数调用" << endl;
}
//拷贝构造函数
SmartPtr(const SmartPtr& another) :CountNum(another.CountNum)
{
++CountNum->count;//只要调用拷贝构造函数,就说明多一个指针指向king对象,所以count++
cout << "Smartptr类拷贝构造函数" << endl;
}
//定义析构函数释放king类对象的资源,
//当记录指向king类对象指针的数量count为0时,释放资源。
~SmartPtr()
{
if (--CountNum->count == 0)
{
delete CountNum;//释放指向成员对象Count的指针
cout << "Smartptr类析构函数" << endl;
}
}
//通过重载“*”和“->”运算符就可以指针的方式实现king类成员的访问。
//->运算符重载,返回指向king类对象的指针。
king *operator->()
{
return CountNum->pking;
}
//*运算符重载,返回king类对象。
king &operator*()
{
return *CountNum->pking;
}
int disCount()
{
return CountNum->count;
}
private:
//成员对象指针
Count *CountNum;
};
void test()
{
king *pstr1 = new king("感谢支持我的博客");
SmartPtr pstr2 = pstr1;//调用有参构造函数,count不++,所以count的默认值才设为1
(*pstr1).Show();
SmartPtr pstr3 = pstr2;//调用拷贝构造函数,count++
pstr2->Show();
cout << "使用基类对象的指针数量:" << pstr2.disCount() << endl;
}
在使用智能指针申请king类对象存储空间后并没有使用delete释放内存空间。使用智能指针可以避免堆内存泄漏,只需申请,无须关注内存是否释放。通过重载“*”和“?>”运算符可以实现对象中成员的访问。
⑧函数调用()运算符重载——仿函数
1.类里有重载函数调用符号的类实例化的对象也叫仿函数。
2.仿函数的作用:
- 方便代码维护
- 方便有权限的调用函数。
- 作为算法的策略(仿函数在STL的算法中使用比较广泛。)
class Maker
{
public:
Maker()
{
name = "强风吹拂king的博客";
}
void printMaker()
{
cout << "感谢支持"+name << endl;
}
//函数调用运算符()重载
void operator()(const string str )
{
cout << str << endl;
}
//函数调用运算符()重载
void operator()(int v1,int v2)
{
cout << v1+v2 << endl;
}
public:
string name;
};
void test()
{
Maker func;
func("感谢支持强风吹拂king的博客");//看起来像函数调用,其实func是对象
func(10, 20);//像调用函数一样调用对象func
func.printMaker();
}