C++核心编程——详解运算符重载

news2024/11/28 14:08:37

在这里插入图片描述

文章目录💬

    • 一.运算符重载基础知识
      • ①基本概念
      • ②运算符重载的规则
      • ③运算符重载形式
      • ④运算符重载建议
    • 二.常用运算符重载
      • ①左移(<<)和右移(>>)运算符重载
        • 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();
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/620142.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Web测试有哪些基本要点?软件测试找第三方软件检测机构靠谱吗?

互联网时代的到来&#xff0c;让Web应用成为了人们生活和工作中不可或缺的一部分。随着Web应用的快速发展&#xff0c;Web测试也变得越来越重要。本文将从Web测试的基本要点和第三方软件检测机构的可靠性两方面进行讨论。 一、Web测试的基本要点 1. 安全性测试&#xff1a;评…

JVM(Java Virtual Machine)

JVM &#x1f50e;内存区域划分Program Counter Register(程序计数器)Native Method Stacks(本地方法栈)JVM Stacks(虚拟机栈)区分虚拟机栈与本地方法栈&#x1f36d;栈是线程私有的&#x1f36d; Heap(堆区)Metaspace(元数据区)总结 &#x1f50e;类加载类加载的流程加载验证准…

【Apache-Flink零基础入门】「入门到精通系列」手把手+零基础带你玩转大数据流式处理引擎Flink(有状态的流式处理)

手把手零基础带你玩转大数据流式处理引擎Flink&#xff08;有状态的流式处理&#xff09; 传统批处理批处理的特点批处理执行原理理想方法 流式处理分布式流式处理有状态分布式流式处理有状态分散式流式处理 总结分析 传统批处理 传统批处理数据是指一种数据处理方式&#xff…

什么是文件描述符以及重定向的本质和软硬链接(Linux)

目录 1 什么是文件&#xff1f;什么是文件操作&#xff1f;认识系统接口open 什么是文件描述符认识Linux底层进程如何打开的文件映射关系重定向的本质理解软硬链接扩展问题 1 什么是文件&#xff1f;什么是文件操作&#xff1f; 文件 文件内容 文件属性&#xff08;文件属性…

暴力递归到动态规划(二)

⭐️前言⭐️ 本篇文章是由暴力递归到动态规划篇章的第二篇。 &#x1f349;欢迎点赞 &#x1f44d; 收藏 ⭐留言评论 &#x1f4dd;私信必回哟&#x1f601; &#x1f349;博主将持续更新学习记录收获&#xff0c;友友们有任何问题可以在评论区留言 &#x1f349;博客中涉及源…

扫码出入库系统在哪些行业使用率最高?服务业绑得最紧密

什么是扫码出入库系统 扫码出入库系统是一种流行的库存管理系统&#xff0c;它使用二维码、条形码或RFID等技术来管理仓库内的物品出入库情况。 使用扫码出入库系统&#xff0c;用户可以通过扫描物品的二维码或条形码&#xff0c;快速地将物品信息录入系统中&#xff0c;同时…

采用sysbench压测mysql详解

文章目录 安装sysbench工具基于sysbench构造测试表和测试数据数据库读写性能测试数据库读性能测试数据库删除性能测试数据库更新索引字段性能测数据库更新非索引字段性能测试数据库插入数据性能测试数据库写性能测试执行完成压测之后可以将run改成cleanup&#xff0c;清除数据 …

LegalAI公开数据集的整理、总结及介绍(持续更新ing…)

诸神缄默不语-个人CSDN博文目录 最近更新日期&#xff1a;2023.6.7 最早更新日期&#xff1a;2023.6.7 文章目录 1. 司法判决预测2. 通用语料3. 其他集成项目4. 推理5. NLU6. NLG1 QA2 文本摘要 7. 信息抽取1 命名实体识别2 句子边界检测&#xff08;分句&#xff09; 1. 司法…

青岛科技大学|物联网工程|物联网定位技术(第二讲)|15:00

目录 物联网定位技术&#xff08;第二讲&#xff09; 1. 卫星的轨道高度与覆盖区域有何关系&#xff0c;试画图给予说明覆盖区地心角与覆盖面积的关系 2. 试给出实际的卫星地面覆盖区和用户空间可视区所对应的半地心角的公式并请给予解释 3. 定位导航卫星为什么一般不采用同…

【集群】LVS负载均衡群集

文章目录 前言一、企业群集应用概述1. 群集的含义1.1 群集的特点1.2 扩展服务器的方式 2. 群集的类型2.1 负载均衡群集&#xff08;Load Balance Cluster&#xff09;2.2 高可用群集&#xff08;High Availability Cluster&#xff09;2.3 高性能运算群集&#xff08;High Perf…

软考A计划-电子商务设计师-模拟试题卷七

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&am…

有一种附件叫做V2附件

大家好&#xff0c;才是真的好。 一般而言&#xff0c;Notes中上传的附件都会对应到某个富文本字段中&#xff0c;这样附件易于处理&#xff0c;也容易进行排版。 最简单的案例就是我们的Notes邮件&#xff0c;附件可以附加在正文中&#xff0c;如下图&#xff1a; 还有我们…

新入职一家公司,接手了个从零开始的项目

开发流程 一个完整的从零开始开发的项目&#xff0c;会涉及到功能设计、数据库设计、项目框架搭建、接口设计与实现等流程&#xff0c;具体可以参考下图。 与我们后端开发有关的主要是功能设计、数据库设计、接口设计与实现这三部分&#xff0c;当然接口设计与实现中也包含项目…

chatgpt赋能python:Python如何快速SEO

Python如何快速SEO Python作为一种通用编程语言&#xff0c;广泛应用于各行各业&#xff0c;包括网站开发和SEO。SEO&#xff08;Search Engine Optimization&#xff09;是通过调整网站的结构和内容来提高其在搜索引擎排名中的位置&#xff0c;从而提高网站的流量和收益。Pyt…

【百问百答】可靠性基础知识第四期

1. IP等级的主要测试标准有哪些? 主要参考标准有GB 4208和IEC 60598&#xff0c; 其中也可以具体参考产品标准&#xff0c; 例如LED灯具参考标准为GB 7000&#xff0c; 汽车电子产品可以参考GB/T 28046.3等等。 2.IP等级可分为几个等级? 根据GB4208标准防止固定异物进入&…

阿里云 Serverless 容器服务全面升级:新增组件全托管、AI 镜像秒级拉取能力

6 月 1 日在阿里云峰会粤港澳大湾区上&#xff0c;阿里云智能云原生应用平台总经理丁宇宣布&#xff0c;Serverless 容器服务 ASK 全面升级&#xff0c;进一步帮助企业和开发者降本提效。 Gartner 曾预测&#xff0c;2023 年 70% 的 AI 应用将基于容器和 Serverless 技术开发。…

开源“上天入地”的本领都在这!2023 开放原子全球开源峰会「开源展览」一文拿捏!

2023 开放原子全球开源峰会 将于 6 月 11-13 日正式举办 开源领域新技术、新应用、新热点 Show Time&#xff01; 前沿开源展览汇 互动体验项目 让每一位参会者融入开源技术新世界&#xff01; 还有精彩的娱乐项目和丰厚礼品 一网打尽&#xff01; 抢先揭秘&#xff0c;一…

【Springboot】基于AOP机制的前置通知以及Cookies记录用户操作日志

文章目录 前言1. 添加依赖2. 创建自定义注解LogAnnotation3. 创建日志记录类型3. 编写切面逻辑4. 完善切面层&#xff0c;获取详细的请求信息4.1 获取自定义注解上的属性值4.2 通过Cookies获取用户信息4.3 获取执行时间4.4 日志实体类以及对应数据库类型 5.最后实现的结果 前言…

Spring Authorization Server扩展实现OAuth2.0的密码模式

写在前面 Spring官方已经停止对Spring Security Oauth2的维护&#xff0c;项目和相关文档也被移除 Spring Authorization Server是官方新推出的OAuth2.1和OpenID Connect1.0的实现 两个主要的版本&#xff0c;0.4.x&#xff1a;jdk8。1.x: jdk17 这里用的版本是0.4.1 OAuth2…

A Comprehensive Survey of Neural Architecture Search: Challenges and Solutions

这是NAS综述系列的第二篇文章&#xff0c;针对《A Comprehensive Survey of Neural Architecture Search:Challenges and Solutions》的翻译。 神经架构搜索综述&#xff1a;挑战与解决方案 摘要1 引言1.1 动机1.2 我们的贡献和相关综述1.3 文章组织 2 早期NAS的特征3 优化策略…