一文说尽C++赋值运算符重载函数(operator=)

http://www.cnblogs.com/zpcdbky/p/5027481.html

在前面:

      关于C++的赋值运算符重载函数(operator=),网络以及各种教材上都有很多介绍,但可惜的是,内容大多雷同且不全面。面对这一局面,在下在整合各种资源及融入个人理解的基础上,整理出一篇较为全面/详尽的文章,以飨读者。

正文:

Ⅰ.举例

例1

复制代码
#include<iostream>
#include<string>
using namespace std;class MyStr
{
private:char *name;int id;
public:MyStr() {}MyStr(int _id, char *_name)   //constructor
    {cout << "constructor" << endl;id = _id;name = new char[strlen(_name) + 1];strcpy_s(name, strlen(_name) + 1, _name);}MyStr(const MyStr& str){cout << "copy constructor" << endl;id = str.id;if (name != NULL)delete name;name = new char[strlen(str.name) + 1];strcpy_s(name, strlen(str.name) + 1, str.name);}MyStr& operator =(const MyStr& str)//赋值运算符
    {cout << "operator =" << endl;if (this != &str){if (name != NULL)delete name;this->id = str.id;int len = strlen(str.name);name = new char[len + 1];strcpy_s(name, strlen(str.name) + 1, str.name);}return *this;}~MyStr(){delete name;}
};int main()
{MyStr str1(1, "hhxx");cout << "====================" << endl;MyStr str2;str2 = str1;cout << "====================" << endl;MyStr str3 = str2;return 0;
}
复制代码

结果:

Ⅱ.参数

一般地,赋值运算符重载函数的参数是函数所在类的const类型的引用(如上面例1),加const是因为

①我们不希望在这个函数中对用来进行赋值的“原版”做任何修改。

②加上const,对于const的和非const的实参,函数就能接受;如果不加,就只能接受非const的实参。

用引用是因为

这样可以避免在函数调用时对实参的一次拷贝,提高了效率。

注意

上面的规定都不是强制的,可以不加const,也可以没有引用,甚至参数可以不是函数所在的对象,正如后面例2中的那样。

Ⅲ.返回值

一般地,返回值是被赋值者的引用,即*this(如上面例1),原因是

①这样在函数返回时避免一次拷贝,提高了效率。

②更重要的,这样可以实现连续赋值,即类似a=b=c这样。如果不是返回引用而是返回值类型,那么,执行a=b时,调用赋值运算符重载函数,在函数返回时,由于返回的是值类型,所以要对return后边的“东西”进行一次拷贝,得到一个未命名的副本(有些资料上称之为“匿名对象”),然后将这个副本返回,而这个副本是右值,所以,执行a=b后,得到的是一个右值,再执行=c就会出错。

注意

这也不是强制的,我们可以将函数返回值声明为void,然后什么也不返回,只不过这样就不能够连续赋值了。

Ⅳ.调用时机

      当为一个类对象赋值(注意:可以用本类对象为其赋值(如上面例1),也可以用其它类型(如内置类型)的值为其赋值,关于这一点,见后面的例2)时,会由该对象调用该类的赋值运算符重载函数。

如上边代码中

str2 = str1;

一句,用str1为str2赋值,会由str2调用MyStr类的赋值运算符重载函数。

需要注意的是

MyStr str2;

str2 = str1;

MyStr str3 = str2;

在调用函数上是有区别的。正如我们在上面结果中看到的那样。

      前者MyStr str2;一句是str2的声明加定义,调用无参构造函数,所以str2 = str1;一句是在str2已经存在的情况下,用str1来为str2赋值,调用的是拷贝赋值运算符重载函数;而后者,是用str2来初始化str3,调用的是拷贝构造函数。

Ⅴ.提供默认赋值运算符重载函数的时机

      当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动生成这样一个赋值运算符重载函数。注意我们的限定条件,不是说只要程序中有了显式的赋值运算符重载函数,编译器就一定不再提供默认的版本,而是说只有程序显式提供了以本类或本类的引用为参数的赋值运算符重载函数时,编译器才不会提供默认的版本。可见,所谓默认,就是“以本类或本类的引用为参数”的意思。

见下面的例2

复制代码
#include<iostream>
#include<string>
using namespace std;class Data
{
private:int data;
public:Data() {};Data(int _data):data(_data){cout << "constructor" << endl;}Data& operator=(const int _data){cout << "operator=(int _data)" << endl;data = _data;return *this;}
};int main()
{Data data1(1);Data data2,data3;cout << "=====================" << endl;data2 = 1;cout << "=====================" << endl;data3 = data2;return 0;
}
复制代码

结果:

     上面的例子中,我们提供了一个带int型参数的赋值运算符重载函数,data2 = 1;一句调用了该函数,如果编译器不再提供默认的赋值运算符重载函数,那么,data3 = data2;一句将不会编译通过,但我们看到事实并非如此。所以,这个例子有力地证明了我们的结论。

Ⅵ.构造函数还是赋值运算符重载函数

     如果我们将上面例子中的赋值运算符重载函数注释掉,main函数中的代码依然可以编译通过。只不过结论变成了

可见,当用一个非类A的值(如上面的int型值)为类A的对象赋值时

如果匹配的构造函数和赋值运算符重载函数同时存在(如例2),会调用赋值运算符重载函数。

如果只有匹配的构造函数存在,就会调用这个构造函数。

Ⅶ.显式提供赋值运算符重载函数的时机

用非类A类型的值为类A的对象赋值时(当然,从Ⅵ中可以看出,这种情况下我们可以不提供相应的赋值运算符重载函数而只提供相应的构造函数来完成任务)。

当用类A类型的值为类A的对象赋值且类A的成员变量中含有指针时,为避免浅拷贝(关于浅拷贝和深拷贝,下面会讲到),必须显式提供赋值运算符重载函数(如例1)。

Ⅷ.浅拷贝和深拷贝

      拷贝构造函数和赋值运算符重载函数都会涉及到这个问题。

      所谓浅拷贝,就是说编译器提供的默认的拷贝构造函数和赋值运算符重载函数,仅仅是将对象a中各个数据成员的值拷贝给对象b中对应的数据成员(这里假设a、b为同一个类的两个对象,且用a拷贝出b或用a来给b赋值),而不做其它任何事。

      假设我们将例1中显式提供的拷贝构造函数注释掉,然后同样执行MyStr str3 = str2;语句,此时调用默认的拷贝构造函数,它只是将str2的id值和nane值拷贝到str3,这样,str2和str3中的name值是相同的,即它们指向内存中的同一区域(在例1中,是字符串”hhxx”)。如下图

                                                   

     这样,会有两个致命的错误

①当我们通过str2修改它的name时,str3的name也会被修改!

②当执行str2和str3的析构函数时,会导致同一内存区域释放两次,程序崩溃!

      这是万万不可行的,所以我们必须通过显式提供拷贝构造函数以避免这样的问题。就像我们在例1中做的那样,先判断被拷贝者的name是否为空,若否,dalete name(后面会解释为什么要这么做),然后,为name重新申请空间,再将拷贝者name中的数据拷贝到被拷贝者的name中。执行后,如图

                                                    

      这样,str2.name和str3.name各自独立,避免了上面两个致命错误。

      我们是以拷贝构造函数为例说明的,赋值运算符重载函数也是同样的道理。

Ⅸ.赋值运算符重载函数只能是类的非静态的成员函数

       C++规定,赋值运算符重载函数只能是类的非静态的成员函数,不能是静态成员函数,也不能是友元函数。关于原因,有人说,赋值运算符重载函数往往要返回*this,而无论是静态成员函数还是友元函数都没有this指针。这乍看起来很有道理,但仔细一想,我们完全可以写出这样的代码

static friend MyStr& operator=(const MyStr str1,const MyStr str2)
{……return str1;
}

      可见,这种说法并不能揭露C++这么规定的原因。

      其实,之所以不是静态成员函数,是因为静态成员函数只能操作类的静态成员,不能操作非静态成员。如果我们将赋值运算符重载函数定义为静态成员函数,那么,该函数将无法操作类的非静态成员,这显然是不可行的。

      在前面的讲述中我们说过,当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动提供一个。现在,假设C++允许将赋值运算符重载函数定义为友元函数并且我们也确实这么做了,而且以类的引用为参数。与此同时,我们在类内却没有显式提供一个以本类或本类的引用为参数的赋值运算符重载函数。由于友元函数并不属于这个类,所以,此时编译器一看,类内并没有一个以本类或本类的引用为参数的赋值运算符重载函数,所以会自动提供一个。此时,我们再执行类似于str2=str1这样的代码,那么,编译器是该执行它提供的默认版本呢,还是执行我们定义的友元函数版本呢?

       为了避免这样的二义性,C++强制规定,赋值运算符重载函数只能定义为类的成员函数,这样,编译器就能够判定是否要提供默认版本了,也不会再出现二义性。

Ⅹ. 赋值运算符重载函数不能被继承

见下面的例3

复制代码
#include<iostream>
#include<string>
using namespace std;class A
{
public:int X;A() {}A& operator =(const int x){X = x;return *this;}    
};
class B :public A
{
public:B(void) :A() {}
};
int main() {A a;B b;a = 45;//b = 67;(A)b = 67;return 0; }
复制代码

      注释掉的一句无法编译通过。报错提示:没有与这些操作数匹配的”=”运算符。对于b = 67;一句,首先,没有可供调用的构造函数(前面说过,在没有匹配的赋值运算符重载函数时,类似于该句的代码可以调用匹配的构造函数),此时,代码不能编译通过,说明父类的operator =函数并没有被子类继承。

     为什么赋值运算符重载函数不能被继承呢?

     因为相较于基类,派生类往往要添加一些自己的数据成员和成员函数,如果允许派生类继承基类的赋值运算符重载函数,那么,在派生类不提供自己的赋值运算符重载函数时,就只能调用基类的,但基类版本只能处理基类的数据成员,在这种情况下,派生类自己的数据成员怎么办?

     所以,C++规定,赋值运算符重载函数不能被继承。

    上面代码中, (A)b = 67; 一句可以编译通过,原因是我们将B类对象b强制转换成了A类对象。

Ⅺ.赋值运算符重载函数要避免自赋值

      对于赋值运算符重载函数,我们要避免自赋值情况(即自己给自己赋值)的发生,一般地,我们通过比较赋值者与被赋值者的地址是否相同来判断两者是否是同一对象(正如例1中的if (this != &str)一句)。

     为什么要避免自赋值呢?

 ①为了效率。显然,自己给自己赋值完全是毫无意义的无用功,特别地,对于基类数据成员间的赋值,还会调用基类的赋值运算符重载函数,开销是很大的。如果我们一旦判定是自赋值,就立即return *this,会避免对其它函数的调用。

如果类的数据成员中含有指针,自赋值有时会导致灾难性的后果。对于指针间的赋值(注意这里指的是指针所指内容间的赋值,这里假设用_p给p赋值),先要将p所指向的空间delete掉(为什么要这么做呢?因为指针p所指的空间通常是new来的,如果在为p重新分配空间前没有将p原来的空间delete掉,会造成内存泄露),然后再为p重新分配空间,将_p所指的内容拷贝到p所指的空间。如果是自赋值,那么p和_p是同一指针,在赋值操作前对p的delete操作,将导致p所指的数据同时被销毁。那么重新赋值时,拿什么来赋?

      所以,对于赋值运算符重载函数,一定要先检查是否是自赋值,如果是,直接return *this。

结束语:

      至此,本文的所有内容都介绍完了。由于在下才疏学浅,错误纰漏之处在所难免,如果您在阅读的过程中发现了在下的错误和不足,请您务必指出。您的批评指正就是在下前进的不竭动力! 


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

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

相关文章

Python a和a[:]的区别

简单来讲a[:]是深复制&#xff0c;a是浅复制&#xff0c;相当于赋值a的话是赋值了指针&#xff0c;赋值a[:]相当于复制了a对应的那段空间 例如&#xff1a; a [1,1,1,1,1,1]for x in a:if x1:a.remove(x)print(a)运行结果&#xff1a; remove操作是移除序列中第一个x元素。…

Linux系统【二】exec族函数及应用

文件描述符 文件描述符表是一个指针数组&#xff0c;文件描述符是一个整数。 文件描述符表对应的指针是一个结构体&#xff0c;名字为file_struct&#xff0c;里面保存的是已经打开文件的信息 需要注意的是父子进程之间读时共享&#xff0c;写时复制的原则是针对物理地址而言…

白话C++系列(27) -- RTTI:运行时类型识别

http://www.cnblogs.com/kkdd-2013/p/5601783.htmlRTTI—运行时类型识别 RTTI&#xff1a;Run-Time Type Identification。 那么RTTI如何来体现呢&#xff1f;这就要涉及到typeid和dynamic_cast这两个知识点了。为了更好的去理解&#xff0c;那么我们就通过一个例子来说明。这个…

使用头文件的原因和规范

原因 通过头文件来调用库功能。在很多场合&#xff0c;源代码不便&#xff08;或不准&#xff09;向用户公布&#xff0c;只 要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库 功能&#xff0c;而不必关心接口怎么实现的。编译器会从库中提取相应…

转圈踢人问题

https://www.cnblogs.com/lanxuezaipiao/p/3339603.html 有N个人围一圈依次报数&#xff0c;数到3的倍数的人出列&#xff0c;问当只剩一个人时他原来的位子在哪里&#xff1f; 解答&#xff1a;经典的转圈踢人问题&#xff0c;好吧专业一点&#xff0c;约瑟夫环问题&#xff0…

Python3常用数据结构

Python3中有三种组合数据类型&#xff0c;分别为&#xff1a; 序列类型&#xff1a;字符串&#xff08;str&#xff09;、元组&#xff08;tuple&#xff09;、列表&#xff08;list&#xff09;集合类型&#xff1a;集合&#xff08;set&#xff09;映射类型&#xff1a;字典…

TCP第四次挥手为什么要等待2MSL

当客户端进入TIME-WAIT状态的时候(也就是第四次挥手的时候)&#xff0c;必须经过时间计数器设置的时间2MSL(最长报文段寿命)后&#xff0c;才能进入关闭状态&#xff0c;这时为什么呢&#xff1f;&#xff1f;&#xff1f; 这最主要是因为两个理由&#xff1a; 1、为了保证客户…

计算机网络【一】概述+OSI参考模型

网络概述 局域网:覆盖范围小(100m以内)&#xff0c;自己花钱买设备&#xff0c;带宽固定(10M,100M,1000M)&#xff0c;自己维护&#xff08;接入层交换机直接连接电脑、汇聚层交换机直接连接接入层交换机&#xff09; 广域网:距离远&#xff0c;花钱买服务&#xff0c;租带宽&…

单链表逆序的多种方式

https://www.cnblogs.com/eniac12/p/4860642.htmltemplate<class T> void List<T>::Inverse() {if(first NULL) return;LinkNode<T> *p, *prev, *latter; p first->link;   // 当前结点prev NULL;   // 前一结点l…

socket编程 -- epoll模型服务端/客户端通信的实现

https://blog.csdn.net/y396397735/article/details/50680359 本例实现如下功能&#xff1a; 支持多客户端与一个服务端进行通信&#xff0c;客户端给服务端发送字符串数据&#xff0c;服务端将字符串中小写转为大写后发送回客户端&#xff0c;客户端打印输出经转换后的字符串。…

Python3 面向对象程序设计

类的定义 Python使用class关键字来定义类 class Car:def infor(self):print("This is a car") car Car() car.infor()内置方法isinstance()来测试一个对象是否为某个类的实例 self参数 类的 所有实例方法都有一个默认的self参数&#xff0c;并且必须是方法的第一…

计算机网络【二】物理层基础知识

计算机网络的性能 速率&#xff1a;连接在计算机网络上的主机在数字信道上传送数据位数的速率&#xff0c;也成为data rate 或bit rate&#xff0c;单位是b/s,kb/s,Mb/s,Gb/s。 我们平时所讲的宽带的速度是以字为单位的&#xff0c;但是实际中应用一般显示的是字节 &#xff0…

Linux网络编程——tcp并发服务器(多进程)

https://blog.csdn.net/lianghe_work/article/details/46503895一、tcp并发服务器概述一个好的服务器,一般都是并发服务器&#xff08;同一时刻可以响应多个客户端的请求&#xff09;。并发服务器设计技术一般有&#xff1a;多进程服务器、多线程服务器、I/O复用服务器等。二、…

求序列第K大算法总结

参考博客&#xff1a;传送门 在上面的博客中介绍了求序列第K大的几种算法&#xff0c;感觉收益良多&#xff0c;其中最精巧的还是利用快速排序的思想O(n)查询的算法。仔细学习以后我将其中的几个实现了一下。 解法 1&#xff1a; 将乱序数组从大到小进行排序然后取出前K大&a…

Linux网络编程——tcp并发服务器(多线程)

https://blog.csdn.net/lianghe_work/article/details/46504243tcp多线程并发服务器多线程服务器是对多进程服务器的改进&#xff0c;由于多进程服务器在创建进程时要消耗较大的系统资源&#xff0c;所以用线程来取代进程&#xff0c;这样服务处理程序可以较快的创建。据统计&a…

计算机网络【三】物理层数据通信

物理层传输媒介 导向传输媒体&#xff0c;比如光纤和铜线 双绞线&#xff08;屏蔽双绞线STP 五屏蔽双绞线UTP&#xff09;电线扭曲在一起可以降低互相之间的电磁干扰 同轴电缆 (50欧姆的基带同轴电缆&#xff0c;75欧姆的宽带同轴电缆) 10M和100M网络只使用了四根线&#xf…

02_算法分析

02_算法分析 0.1 算法的时间复杂度分析0.1.1 函数渐近增长概念&#xff1a;输入规模n>2时&#xff0c;算法A1的渐近增长小于算法B1 的渐近增长随着输入规模的增大&#xff0c;算法的常数操作可以忽略不计测试二&#xff1a;随着输入规模的增大&#xff0c;与最高次项相乘的常…

Linux网络编程——I/O复用之select详解

https://blog.csdn.net/lianghe_work/article/details/46506143一、I/O复用概述I/O复用概念&#xff1a;解决进程或线程阻塞到某个 I/O 系统调用而出现的技术&#xff0c;使进程不阻塞于某个特定的 I/O 系统调I/O复用使用的场合&#xff1a;1.当客户处理多个描述符&#xff08;…

Linux网络编程——tcp并发服务器(I/O复用之select)

https://blog.csdn.net/lianghe_work/article/details/46519633与多线程、多进程相比&#xff0c;I/O复用最大的优势是系统开销小&#xff0c;系统不需要建立新的进程或者线程&#xff0c;也不必维护这些线程和进程。代码示例&#xff1a;#include <stdio.h> #include &l…