欢迎光临散文网 会员登陆 & 注册

C++自制心得——类与对象中(拷贝构造+赋值重载)

2023-09-06 13:11 作者:这年头起名可真不容易  | 我要投稿

在开始我们今天的内容之前,给大家介绍一个小东西,cout和cin,流插入流提取函数。

该函数的前置为头文件iostream,并展开命名空间std,这两个函数可替代printf,scanf的功能,而且使用体验比前者好得多。在这里左移操作符被重载为流插入函数,右移操作符被重载为流提取函数(运算符重载马上就会讲,不要着急),所以cin >> a的意思就是从控制台提取对象赋给a,cout << a << endl的意思是把a的值打印在屏幕上并换行,先讲这么多,运算符重载会再讲一点,后续我会专门出一期iostream的专栏。

拷贝构造函数

又是一段新代码,让我们看看它的运行结果

哦豁,炸掉了,调试一下

噢,编译器释放了一个野指针,所以程序崩了。这里我们引入一个新概念,深浅拷贝。

浅拷贝就是值拷贝,你的值是什么,我的值就是什么,C语言用的就是浅拷贝,一般情况下没有什么问题,但在这里问题很明显

浅拷贝会导致两个数组的指针指向同一块空间,而在C++里析构函数会在对象生命周期结束后自动调用,因此对象s出了func作用域就会调用一次析构函数,导致对象s1的数组指针变为野指针,紧接着s1出作用域再次调用析构函数,导致程序崩溃。

深拷贝不仅仅是值拷贝,他还会拷贝整个数据的结构,正因如此,我们希望用深拷贝替代浅拷贝(对于复杂结构,浅拷贝基本没有存在价值,引用比它好用得多),那有没有办法自动进行深拷贝?拷贝构造函数与复制重载函数可以解决这个问题,前者针对对象的初始化,后者针对对象的赋值。

我们先介绍拷贝构造函数

拷贝构造函数的几个基本特点

1. k拷贝构造函数只适用于对象的初始化,如果是对象的赋值,则不会调用构造函数

2. 拷贝构造函数函数名同构造函数,均为类名,构成函数重载

3. 拷贝构造函数形参不得出现该类的对象,这会导致死循环递归(有的编译器会直接报错)

我来解释一下,Stack对象的传参行为本质上就是用已存在对象给目标对象初始化,按照规则,我们需要调用拷贝构造函数,可如果那个函数就是拷贝构造自身,拷贝构造函数就会再次调用自身,陷入死循环

4. 构造函数有多种调用方式

其余代码同上

这三种调用方式均视为调用拷贝构造函数,注意Stack s3 = s2不会调用赋值重载函数,因为本质是初始化。

5. 如用户未显式定义,编译器会生成构造函数,其有以下特点,对内置类型进行浅拷贝,对自定义类型调用其构造函数。

写到这想起一件事情,记得给拷贝构造加const,不要学我,容易被坑死。

6. 拷贝构造函数也存在初始化列表,因为以拷贝构造形式初始化的对象不会调用构造函数,而是直接调用拷贝构造函数

赋值重载函数

1. 运算符重载

在C语言阶段,我们一直缺乏一种直观的方式来表达自定义类型的基本运算,比方说如果我们想写一个日期+天数,那我们就只能以函数调用的形式来完成相关计算。函数肯定是要写的,不过我们能不能换一种表达形式,把函数调用的形式转换为运算符的形式,显然,从代码的易读性和美观性来讲运算符比传统的函数调用好得多。C++提供了解决方案,就是运算符重载。

date.h

date.cpp

把函数名换成operator加对应重载运算符就可以实现运算符重载了

test.cpp

用法十分亲民,一看就懂。

运算符重载本质上就是把运算符作为函数使用,所以上面六个cout也可以这么写

运算符重载函数有如下特性:

1. 不可以用这种方式创造新运算符(operator@就是非法函数)

运算符重载,顾名思义,意义的再次扩展,显然只有有意义的运算符,才能重载。

2. 函数形参个数与运算符操作数个数必须保持严格一致(自加自减除外,因为在自加自减这一情景下,该规则与函数重载规则冲突,需要额外的占位形参解决问题)

这是很自然的结论,比如“+”操作符需要两个操作数,你重载的“+”函数形参数目不对等,你能认为你重载的函数在含义上与“+”保持一致吗?

注:因为隐含this指针的存在,成员函数的形参会少一个

3. 函数形参中必须包含自定义类型(内置类型不用你再造一次轮子)

4. .*  ::  sizeof  ?:  .  以上五个运算符不能重载

运算符重载用好了能做出什么效果?你觉得一个可以一键赋值,无元素上限的数组怎么样。

test.h

slist.h

slist.cpp

通过对方括号运算符的重载,再结合四大成员函数,我们成功的把顺序表改造成了数组,而这个新数组既拥有顺序表的动态扩容能力,也保留了数组的灵活性。这个改造,可以说相当成功

接下来,我以日期类为例给各位讲解常用运算符重载设计的一些技巧

date.h

date.cpp

这里有两个地方看起来很奇怪,一个是友元函数(流插入流提取运算符的重载函数),另一个就是某些成员函数后加的const,我们在下一节会详细讲解这些,现在浅浅提一笔。

如果C++允许this指针在成员函数中显式写出,并允许有限修改,那我们就不需要写这些补丁了。

第一个,友元函数,是为了解决Date的this指针不能换位的问题。显然,<<与>>的本质还是函数,而这两个函数的具体功能分别在ostream类与istream类里实现,所以套壳换皮的我们就要用到ostream与istream的对象,那么问题就出现了,this指针一定占据形参的第一位,但是<<与>>运算符在正常使用时的第一个参数不是日期类的this指针,而是库类的对象。为了能够正常使用该函数,只能把它定义成全局。但定义成全局函数,就必须要突破private限制,解决办法有二,一是提供get函数,二就是定义成友元函数

第二个,悬空const,是为了解决Date的this指针不能正常被const修饰的问题。const对象只能传递const this指针,显然不能把它转换成this,不然权限就要乱套了。但是this指针不能显式写出,我没办法给它加const修饰,只能悬空写

1. 比较运算符只需要写>, ==或者<, ==函数,剩下的四个都能代码复用,我这里手写了三个主要是为了满足我的强迫症。

2. +=与+,-=与-,这两组只需要写一个,另一个就能代码复用

3. 在2的情境下,我建议手搓前一个而不是后一个,因为效率更高

以operator+为例,这个函数需要定义一个额外对象,并返回一个对象。那么对象tem调用一次拷贝构造函数,返回时构造的临时常变量再次调用拷贝构造,合计两次

但是operator+=的函数并不需要创建额外对象,也不需要返回对象,没有构造系函数带来的空间与时间开销

到这里,结论很明显,令operator+调用operator+=相比用operator+=调用operator+效率更高。

4. 拥有占位符的operator++或--会被编译器识别为后置++或--,而没有占位符的会被识别为前置++或--,同样的,前置++或--比后置++或--效率更高。

2. 赋值重载函数

operator=函数比较特别,与我们讲的前三个成员函数长得一点不像。

赋值重载函数具有如下特点:

1. 只适用于对象间的赋值操作,因此也不具有初始化列表的功能

如果你的对象有某些成员不能赋值就不要用下面的法子拷贝

2. 作为默认成员函数,如未显式定义将由编译器隐式生成,隐式生成的函数对内置类型完成浅拷贝,调用自定义类型的赋值重载函数

3. operator=作为双目操作符,其重载函数必须拥有两个形参(第一个给了this指针)

至于用法,operator=是怎么用的,它就怎么用(初始化除外)

下一个专栏正式结束类与对象的初步认识,内存管理,vector,list,stl的内容正在学习,很快就能和大家见面

C++自制心得——类与对象中(拷贝构造+赋值重载)的评论 (共 条)

分享到微博请遵守国家法律