2024年C++ 运算符重载_c++ 重载=(1),今年最新整理的《高频C C++面试题集合》

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

double real, imag;
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { }
Complex operator - (const Complex & c);

};
Complex operator + (const Complex & a, const Complex & b)
{
return Complex(a.real + b.real, a.imag + b.imag); //返回一个临时对象
}
Complex Complex::operator - (const Complex & c)
{
return Complex(real - c.real, imag - c.imag); //返回一个临时对象
}
int main()
{
Complex a(4, 4), b(1, 1), c;
c = a + b; //等价于 c = operator + (a,b);
cout << c.real << “,” << c.imag << endl;
cout << (a - b).real << “,” << (a - b).imag << endl; //a-b等价于a.operator - (b)
return 0;
}


程序的输出结果是:



> 
> 5,5  
>  3,3
> 
> 
> 


程序将`+`重载为一个全局函数(只是为了演示这种做法,否则重载为成员函数更好),将`-`重载为一个成员函数。  
  
 运算符重载为全局函数时,参数的个数等于运算符的目数(即操作数的个数);运算符重载为成员函数时,参数的个数等于运算符的目数减一。  
  
 如果`+`没有被重载,第 21 行会编译出错,因为编译器不知道如何对两个 Complex 对象进行`+`运算。有了对`+`的重载,编译器就将`a+b`理解为对运算符函数的调用,即`operator+(a,b)`,因此第 21 行就等价于:



c = operator+(a, b);


即以两个操作数 a、b 作为参数调用名为`operator+`的函数,并将返回值赋值给 c。  
  
 第 12 行,在 C++ 中,“类名(构造函数实参表)”这种写法表示生成一个临时对象。该临时对象没有名字,生存期就到包含它的语句执行完为止。因此,第 12 行实际上生成了一个临时的 Complex 对象作为 return 语句的返回值,该临时对象被初始化为 a、b 之和。第 16 行与第 12 行类似。  
  
 由于`-`被重载为 Complex 类的成员函数,因此,第 23 行中的`a-b`就被编译器处理成:



a.operator-(b);


由此就能看出,为什么运算符重载为成员函数时,参数个数要比运算符目数少 1 了。


## 2 C++重载=(C++重载赋值运算符)


赋值运算符`=`要求左右两个操作数的类型是匹配的,或至少是兼容的。有时希望`=`两边的操作数的类型即使不兼容也能够成立,这就需要对`=`进行重载。[C++]( ) 规定,`=`只能重载为成员函数。来看下面的例子。  
  
 要编写一个长度可变的字符串类 String,该类有一个 char\* 类型的成员变量,用以指向动态分配的存储空间,该存储空间用来存放以`\0`结尾的字符串。String 类可以如下编写:



#include
#include
using namespace std;
class String {
private:
char * str;
public:
String() :str(NULL) { }
const char * c_str() const { return str; };
String & operator = (const char * s);
~String();
};
String & String::operator = (const char * s)
//重载"="以使得 obj = "hello"能够成立
{
if (str)
delete[] str;
if (s) { //s不为NULL才会执行拷贝
str = new char[strlen(s) + 1];
strcpy(str, s);
}
else
str = NULL;
return *this;
}
String::~String()
{
if (str)
delete[] str;
};
int main()
{
String s;
s = “Good Luck,”; //等价于 s.operator=(“Good Luck,”);
cout << s.c_str() << endl;
// String s2 = “hello!”; //这条语句要是不注释掉就会出错
s = “Shenzhou 8!”; //等价于 s.operator=(“Shenzhou 8!”);
cout << s.c_str() << endl;
return 0;
}


程序的运行结果:



> 
> Good Luck,  
>  Shenzhou 8!
> 
> 
> 


第 8 行的构造函数将 str 初始化为 NULL,仅当执行了 operator= 成员函数后,str 才会指向动态分配的存储空间,并且从此后其值不可能再为 NULL。在 String 对象的生存期内,有可能从未执行过 operator= 成员函数,所以在析构函数中,在执行delete[] str之前,要先判断 str 是否为 NULL。  
  
 第 9 行的函数返回了指向 String 对象内部动态分配的存储空间的[指针]( ),但是不希望外部得到这个指针后修改其指向的字符串的内容,因此将返回值设为 const char\*。这样,假定 s 是 String 对象,那么下面两条语句编译时都会报错,s 对象内部的字符串就不会轻易地从外部被修改了 :



char* p = s.c_str ();
strcpy(s.c_str(), “Tiangong1”);


第一条语句出错是因为`=`左边是 char\* 类型,右边是 const char \* 类型,两边类型不匹配;第二条语句出错是因为 strcpy 函数的第一个形参是 char\* 类型,而这里实参给出的却是 const char \* 类型,同样类型不匹配。  
  
 如果没有第 13 行对`=`的重载,第 34 行的`s = "Good Luck,"`肯定会因为类型不匹配而编译出错。经过重载后,第 34 行等价`于**s.operator=("Good Luck,");**`,就没有问题了。  
  
 在 operator= 函数中,要先判断 str 是否已经指向动态分配的存储空间,如果是,则要先释放那片空间,然后重新分配一片空间,再将参数 s 指向的内容复制过去。这样,对象中存放的字符串就和 s 指向的字符串一样了。分配空间时,要考虑到字符串结尾的`\0`,因此分配的字节数要比 strlen(s) 多 1。  
  
 需要注意一点,即使对=做了重载,第 36 行的`**String s2 = "hello!"**;`还是会编译出错,因为这是一条初始化语句,要用到构造函数,而不是赋值运算符=。String 类没有编写参数类型为 char \* 的构造函数,因此编译不能通过。  
  
 就上面的程序而言,对 operator= 函数的返回值类型没有什么特别要求,void 也可以。但是在对运算符进行重载时,好的风格是应该尽量保留运算符原本的特性,这样其他人在使用这个运算符时才不容易产生困惑。赋值运算符是可以连用的,这个特性在重载后也应该保持。即下面的写法应该合法:



a = b = c;


假定 a、b、c 都是 String 对象,则上面的语句等价于下面的嵌套函数调用:



a.operator=( b.operator=© );


如果 operator= 函数的返回值类型为 void,显然上面这个嵌套函数调用就不能成立。将返回值类型改为 String 并且返回 \*this 可以解决问题,但是还不够好。因为,假设 a、b、c 是基本类型的变量,则



(a =b) = c;


这条语句执行的效果会使得 a 的值和 c 相等,即`a = b`这个表达式的值其实是 a 的引用。为了保持=的这个特性,operator= 函数也应该返回其所作用的对象的引用。因此,返回值类型为 String & 才是风格最好的写法。在 a、b、c 都是 String 对象时,`(a=b)=c;`等价于



( a.operator=(b) ).operator=©;


a.operator=(b) 返回对 a 的引用后,通过该引用继续调用 operator=(c),就会改变 a 的值。


## 3 C++深拷贝和浅拷贝(C++深复制和浅复制)


同类对象之间可以通过赋值运算符`=`互相赋值。如果没有经过重载,`=`的作用就是把左边的对象的每个成员变量都变得和右边的对象相等,即执行逐个字节拷贝的工作,这种拷贝叫作“浅拷贝”。  
  
 有的时候,两个对象相等,从实际应用的含义上来讲,指的并不应该是两个对象的每个字节都相同,而是有其他解释,这时就需要对`=`进行重载。  
  
 上节我们定义了 String 类,并重载了`=`运算符,使得 char \* 类型的字符串可以赋值给 String 类的对象。完整代码如下:



#include
#include
using namespace std;
class String {
private:
char * str;
public:
String() :str(NULL) { }
const char * c_str() const { return str; };
String & operator = (const char * s);
~String();
};
String & String::operator = (const char * s)
//重载"="以使得 obj = "hello"能够成立
{
if (str)
delete[] str;
if (s) { //s不为NULL才会执行拷贝
str = new char[strlen(s) + 1];
strcpy(str, s);
}
else
str = NULL;
return *this;
}
String::~String()
{
if (str)
delete[] str;
};
int main()
{
String s;
s = “Good Luck,”; //等价于 s.operator=(“Good Luck,”);
cout << s.c_str() << endl;
// String s2 = “hello!”; //这条语句要是不注释掉就会出错
s = “Shenzhou 8!”; //等价于 s.operator=(“Shenzhou 8!”);
cout << s.c_str() << endl;
return 0;
}


对于上面的代码,如果让两个 String 对象相等(把一个对象赋值给另一个对象),其意义到底应该是什么呢?是两个对象的 str 成员变量都指向同一个地方,还是两个对象的 str 成员变量指向的内存空间中存放的内容相同?如果把 String 对象理解为存放字符串的对象,那应该是后者比较合理和符合习惯,而前者不但不符合习惯,还会导致程序漏洞。  
  
 按照上面代码中 String 类的写法,下面的程序片段会引发问题:



String s1, s2;
s1 = “this”;
s2 = “that”;
s2 = s1;


执行完上面的第 3 行后,s1 和 s2 的状态如图 1 (a) 所示,它们的 str 成员变量指向不同的存储空间。  
  


![](https://img-blog.csdnimg.cn/20181226215243470)


  
                                                                                图1:浅拷贝导致的错误


**`s2=s1;`**执行的是浅拷贝。执行完**`s2=s1;`**后,**s2.str** 和**s1.str** 指向同一个地方, 如图 1 (b) 所示。这导致 **s2.str** 原来指向的那片动态分配的存储空间再也不会被释放,变成内存垃圾。  
  
 此外,s1 和 s2 消亡时都会执行**`delete[] str;`**,这就使得同一片存储空间被释放两次,会导致严重的内存错误,可能引发程序意外中止。  
  
 而且,如果执行完`s1=s2;`后 又执行`s1 = "some";`,则会导致 s2.str 也被释放。  
  
 为解决上述问题,需要对做`=`再次重载。重载后的的逻辑,应该是使得执行`s2=s1;`后,s2.str 和 s1.str 依然指向不同的地方,但是这两处地方所存储的字符串是一样的。再次重载`=`的写法如下:



String & String::operator = (const String & s)
{
if(str == s.str)
return * this;
if(str)
delete[] str;
if(s.str){ //s. str不为NULL才执行复制操作
str = new char[ strlen(s.str) + 1 ];
strcpy(str, s.str);
}
else
str = NULL;
return * this;
}


经过重载,赋值号`=`的功能不再是浅拷贝,而是将一个对象中[指针]( )成员变量指向的内容复制到另一个对象中指针成员变量指向的地方。这样的拷贝就叫“深拷贝”。  
  
 程序第 3 行要判断 **str==s.str**,是因为要应付如下的语句:



s1 = s1;


这条语句本该不改变s1的值才对。**`s1=s1;`**等价于`**s.operator=(s1)**;`,如果没有第 3 行和第 4 行,就会导致函数执行中的 str 和 s.str 完全是同一个指针(因为形参 s 引用了实参 s1,因此可以说 s 就是 s1)。第 8 行为 str 新分配一片存储空间,第 9 行从自己复制到自己,那么 str 指向的内容就不知道变成什么了。  
  
 当然,程序员可能不会写`s1=s1;`这样莫名奇妙的语句,但是可能会写`rs1=rs2;`,如果 rs1 和 rs2 都是 String 类的引用,而且它们正好引用了同一个 String 对象,那么就等于发生了`s1=s1;`这样的情况。  
  
**思考题:上面的两个 operator= 函数有什么可以改进以提高执行效率的地方?**  
  
 重载了两次`=`的 String 类依然可能导致问题。因为没有编写复制构造函数,所以一旦出现使用复制构造函数初始化的 String 对象(例如,String 对象作为函数形参,或 String 对象作为函数返回值),就可能导致问题。最简单的可能出现问题的情况如下:



String s2;
s2 = “Transformers”;
String s1(s2);


s1 是以 s2 作为实参,调用默认复制构造函数来初始化的。默认复制构造函数使得 s1.str 和 s2.str 指向同一个地方,即执行的是浅拷贝,这就导致了前面提到的没有对`=`进行第二次重载时产生的问题。因此还应该为 String 类编写如下复制构造函数,以完成深拷贝:



String::String(String & s)
{
if(s.str){
str = new char[ strlen(s.str) + 1 ];
strcpy(str, s.str);
}
else
str = NULL;
}



最后,给出 String 类的完整代码:



class String {
private:
char * str;
public:
String() :str(NULL) { }
String(String & s);
const char * c_str() const { return str; };
String & operator = (const char * s);
String & operator = (const String & s);
~String();
};
String::String(String & s)
{
if (s.str) {
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
}
else
str = NULL;
}
String & String::operator = (const String & s)
{
if (str == s.str)
return *this;
if (str)
delete[] str;
if (s.str) { //s. str不为NULL才执行复制操作
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
}
else
str = NULL;
return *this;
}
String & String::operator = (const char * s)
//重载"="以使得 obj = "hello"能够成立
{
if (str)
delete[] str;
if (s) { //s不为NULL才会执行拷贝
str = new char[strlen(s) + 1];
strcpy(str, s);
}
else
str = NULL;
return *this;
}
String::~String()
{
if (str)
delete[] str;
};


## 4 C++运算符重载为友元函数


一般情况下,将运算符重载为类的成员函数是较好的选择。但有时,重载为成员函数不能满足使用要求,重载为全局函数又不能访问类的私有成员,因此需要将运算符重载为友元。  
  
 例如,对于复数类 Complex 的对象,希望它能够和整型以及实数型数据做四则运算,假设 c 是 Complex 对象,希望`c+5`和`5+c`这两个表达式都能解释得通。  
  
 将+重载为 Complex 类的成员函数能解释`c+5`,但是无法解释`5+c`。要让`5+c`有意义,则应对+进行再次重载,将其重载为一个全局函数。为了使该全局函数能访问 Complex 对象的私有成员,就应该将其声明为 Complex 类的友元。具体写法如下:



class Complex
{
double real, imag;
public:
Complex(double r, double i):real®, imag(i){};
Complex operator + (double r);
friend Complex operator + (double r, const Complex & c);
};
Complex Complex::operator + (double r)
{ //能解释c+5
return Complex(real+r, imag);
}
Complex operator + (double r, const Complex & c)
{ //能解释5+c
return Complex (c.real+r, c.imag);
}


##  5 C++实现可变长度的动态数组


实践中经常碰到程序需要定义一个数组,但不知道定义多大合适的问题。按照最大的可能性定义,会造成空间浪费;定义小了则无法满足需要。  
  
 如果用动态内存分配的方式解决,需要多少空间就动态分配多少,固然可以解决这个问题,但是要确保动态分配的内存在每一条执行路径上都能够被释放,也是一件头疼的事情。  
  
 因此需要编写一个长度可变的数组类,该类的对象就能存放一个可变长数组。该数组类应该有以下特点:


* 数组的元素个数可以在初始化该对象时指定。
* 可以动态往数组中添加元素。
* 使用该类时不用担心动态内存分配和释放问题。
* 能够像使用数组那样使用动态数组类对象,如可以通过下标访问其元素。


程序代码如下:



#include
#include
using namespace std;
class CArray
{
int size; //数组元素的个数
int* ptr; //指向动态分配的数组
public:
CArray(int s = 0); //s代表数组元素的个数
CArray(CArray & a);
~CArray();
void push_back(int v); //用于在数组尾部添加一个元素 v
CArray & operator = (const CArray & a); //用于数组对象间的赋值
int length() const { return size; } //返回数组元素个数
int & operator[](int i)
{ //用以支持根据下标访问数组元素,如“a[i]=4;”和“n=a[i];”这样的语句
return ptr[i];
};
};
CArray::CArray(int s) : size(s)
{
if (s == 0)
ptr = NULL;
else
ptr = new int[s];
}
CArray::CArray(CArray & a)
{
if (!a.ptr) {
ptr = NULL;
size = 0;
return;
}
ptr = new int[a.size];
memcpy(ptr, a.ptr, sizeof(int) * a.size);
size = a.size;
}
CArray::~CArray()
{
if (ptr) delete[] ptr;
}
CArray & CArray::operator=(const CArray & a)
{ //赋值号的作用是使 = 左边对象中存放的数组的大小和内容都与右边的对象一样
if (ptr == a.ptr) //防止 a=a 这样的赋值导致出错
return *this;
if (a.ptr == NULL) { //如果a里面的数组是空的
if (ptr)
delete[] ptr;
ptr = NULL;
size = 0;
return *this;
}
if (size < a.size) { //如果原有空间够大,就不用分配新的空间
if (ptr)
delete[] ptr;
ptr = new int[a.size];
}
memcpy(ptr, a.ptr, sizeof(int)*a.size);
size = a.size;
return this;
}
void CArray::push_back(int v)
{ //在数组尾部添加一个元素
if (ptr) {
int
tmpPtr = new int[size + 1]; //重新分配空间
memcpy(tmpPtr, ptr, sizeof(int) * size); //复制原数组内容
delete[] ptr;
ptr = tmpPtr;
}
else //数组本来是空的
ptr = new int[1];
ptr[size++] = v; //加入新的数组元素
}
int main()
{
CArray a; //开始的数组是空的
for (int i = 0; i<5; ++i)
a.push_back(i);
CArray a2, a3;
a2 = a;
for (int i = 0; i<a.length(); ++i)
cout << a2[i] << " ";
a2 = a3; //a2 是空的
for (int i = 0; i<a2.length(); ++i) //a2.length()返回 0
cout << a2[i] << " ";
cout << endl;
a[3] = 100;
CArray a4(a);
for (int i = 0; i<a4.length(); ++i)
cout << a4[i] << " ";
return 0;
}


程序的输出结果为:



> 
> 0 1 2 3 4  
>  0 1 2 100 4
> 
> 
> 


`[]`是双目运算符,有两个操作数,一个在里面,一个在外面。表达式 a[i] 等价于 a.operator[](i)。按照`[]`原有的特性,`a[i]`应该能够作为左值使用,因此 operator[] 函数应该返回引用。  
  
**思考题:每次在数组尾部添加一个元素都要重新分配内存并且复制原有内容,显然效率是低下的。有什么办法能够加快添加元素的速度呢?**


## 6 C++重载<<和>>(C++重载输出运算符和输入运算符)


在 [C++]( ) 中,左移运算符`<<`可以和 cout 一起用于输出,因此也常被称为“流插入运算符”或者“输出运算符”。实际上,`<<`本来没有这样的功能,之所以能和 cout 一起使用,是因为被重载了。  
  
 cout 是 ostream 类的对象。ostream 类和 cout 都是在头文件 <iostream> 中声明的。ostream 类将`<<`重载为成员函数,而且重载了多次。为了使`cout<<"Star War"`能够成立,ostream 类需要将`<<`进行如下重载:


![img](https://img-blog.csdnimg.cn/img_convert/aaa39c22166e893c787c785708d3e57e.png)
![img](https://img-blog.csdnimg.cn/img_convert/4898432bb52eea4ae46dd315ae71818f.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**

C++重载输出运算符和输入运算符)


在 [C++]( ) 中,左移运算符`<<`可以和 cout 一起用于输出,因此也常被称为“流插入运算符”或者“输出运算符”。实际上,`<<`本来没有这样的功能,之所以能和 cout 一起使用,是因为被重载了。  
  
 cout 是 ostream 类的对象。ostream 类和 cout 都是在头文件 <iostream> 中声明的。ostream 类将`<<`重载为成员函数,而且重载了多次。为了使`cout<<"Star War"`能够成立,ostream 类需要将`<<`进行如下重载:


[外链图片转存中...(img-rRSsXaRi-1715568332676)]
[外链图片转存中...(img-GpBAYGJM-1715568332676)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**

  • 30
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值