【C++】—— 类与对象(三)

【C++】—— 类与对象(三)

  • 4、拷贝构造函数
    • 4.1、初识拷贝构造
      • 4.1.1、为什么要传引用
      • 4.1.2、引用尽量加上 const
    • 4.2、深入拷贝构造
      • 4.2.1、为什么要自己实现拷贝构造
      • 4.2.2、传值返回先调用拷贝构造的原因
      • 4.2.3、躺赢的 MyQueue
      • 4.2.4、传值返回与引用返回
    • 4.3、总结
  • 5、取地址运算符重载
    • 5.1、const 成员函数
    • 5.2、取地址运算符重载

4、拷贝构造函数

4.1、初识拷贝构造

  我们要先知道,拷贝构造是一个特殊的构造函数
  拷贝构造的作用是:用一个自身类的对象初始化 当前的对象

  
拷贝构造基本特点

  • 拷贝构造函数是构造函数的一个重载
  • 拷贝构造函数的第一个参数必须是自身类类型的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。其他任何额外的参数都要有缺省值(默认值)
  • C++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参传值返回都会调用拷贝构造完成

  
我们先用 D a t e Date Date 类型来感受一下拷贝构造:

在这里插入图片描述

运行结果:

在这里插入图片描述

  
拷贝构造的 调用方式 有两种:

  • 一种是像上述代码一样类似于构造函数的调用方法Date d2(d1);
  • 另一种是类似赋值的调用:Date d2 = d1;

  

4.1.1、为什么要传引用

  • C++ 规定:对自定义类型传值传参要先调用拷贝构造
class date
{
public:date(int year, int month, int day):_year(year), _month(month), _day(day){}date(const date& d){*this = d;}private:int _year;int _month;int _day;
};void func(const date d)
{cout << "heallo world" << endl;
}int main()
{date d1(2024, 1, 1);func(d1);return 0;
}

  
我们来调试来验证 一下:

在这里插入图片描述
对自定义类型,传值传参都会调用拷贝构造函数。

  
  那这样的话,拷贝构造用传值传参会发生什么呢?
  答: 无穷递归

  
  我们通过图来理解一下:

在这里插入图片描述

  
  而如果是传引用的话, d d d d 1 d1 d1 的别名,就不会形成新的拷贝构造。

 所以,对于自定义类型,传参都不建议使用传值传参。用传值传参还需要先调用拷贝构造,尤其是当实参特别大时,太费劲了

  为什么传值传参要先调用拷贝构造呢?别急,我们学习完下一个知识点就来回答

  

4.1.2、引用尽量加上 const

  使用引用传参,当函数体不需要改变外面的实参时,尽量都使用 c o n s t const const 引用!

  • 因为加上 c o n s t const const 可以保护形参不被改变

  假设,我要写一个判断逻辑,结果 “==” 不小心写成了 “=”。如果没加 c o n s t const const,那形参 d d d 就真被改了,我去给别人拷贝,结果我自己被改了,这合适吗?

Date(Date& d)
{if (d._year = _year){//···}
}

  

  • 而且,不使用 c o n s t const const 引用,当传的实参是只读性质时,会造成权限放大,编译不过去。使用了 c o n s t const const无论实参是不是只读,都能编过去

  

4.2、深入拷贝构造

拷贝构造的进阶特性:

  • 若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用它的拷贝构造
      
  • 传值返回会产生一个临时对象,产生临时对象会调用拷贝构造传引用返回,返回的是返回对象的别名(引用),没有产生拷贝
      
  • 与前面的构造函数析构函数不同,如果我们没有显式实现,编译器默认生成的拷贝构造会对内置类型进行处理,会对内置类型进行值拷贝/浅拷贝。所谓值拷贝(也叫浅拷贝)就是一个字节一个字节进行拷贝,相当于 m e m c p y memcpy memcpy函数的功能。

  

4.2.1、为什么要自己实现拷贝构造

  那默认生成的拷贝构造不是挺好的吗,它都给你完成拷贝了,那我们还需要自己写吗?
  
  我们来看下面这种情况:
  现在,我们实现一个栈类,栈类的成员变量都是内置类型,看看编译器生成的拷贝构造能不能完成任务

typedef int STDataType;
class Stack
{
public :Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;} _capacity = n;_top = 0;} ~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};int main()
{Stack st1(10);//两种都行//Stack st2(st1);Stack st2 = st1return 0;
}

我们调试来看一下:

在这里插入图片描述

  好像没有问题, s t 2 st2 st2 是完成了初始化的。
  
  但我们继续往下运行,发现程序崩了

在这里插入图片描述

  为什么呢?
  因为编译器 仅仅完成了值拷贝/浅拷贝
  
  对于_ t o p top top 和 _ c a p a c i t y capacity capacity 来说他们并没有指向什么资源,只进行值拷贝/浅拷贝没有问题
  但对于 _ a a a 来说,虽然他是内置类型,但是 它指向一块开辟的空间 s t 1 st1 st1 的 _ a a a 中存放的是指向的块空间的地址,将 s t 1 st1 st1 中 _ a a a 的值拷贝给 s t 2 st2 st2 的 _a,此时 s t 2 st2 st2 的 _ a a a 也存这那块空间的地址。也就是说 s t 1 st1 st1 s t 2 st2 st2 的 _ a a a 指向同一块空间
  

在这里插入图片描述

  我们本来想的是他们指向不同的空间,空间中存放的是不同的数据(虽然现在没放数据)。虽然现在和我们想的有点不一样,但指向同一块空间也不至于让程序崩溃啊
  答案出现在析构函数那。

   m a i n main main 函数结束,要销毁两个对象,销毁对象前先调用自身析构函数
  后定义的先析构 s t 2 st2 st2 调用自身析构,将 _ a a a 指向的空间释放,后 s t 1 st1 st1 也调用析构,也要对 _ a a a 指向的空间进行释放,但此时空间已经被释放掉了,也就是说 同一块空间被释放了两次,自然程序崩溃了。
  其实,不仅仅是析构两次,它的问题是很多的。比如:在函数内插入一个 1,再在函数外插入一个 2,2 会将 1 给覆盖
  

  现在,我们自己给它加上拷贝构造函数

	Stack(const Stack& st){// 需要对_a指向资源创建同样⼤的资源再拷⻉值_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (nullptr == _a){perror("malloc申请空间失败!!!");return;} memcpy(_a, st._a, sizeof(STDataType) * st._top);_top = st._top;_capacity = st._capacity;}

  

4.2.2、传值返回先调用拷贝构造的原因

  现在,我们可以回答为什么传值传参要先调用拷贝构造了

  想一想,传值传参传的是实参的拷贝,但这拷贝仅仅只是浅拷贝。像是拷贝上面的 s t a c k stack stack 类,要是在函数里面将 _ a a a 空间释放了,而你以为是传值传参,里面不影响外面,在外面再次将 _ a a a 指向的空间释放,程序就崩溃了。而 C语言只有浅拷贝,是很坑
  所以 C++ 规定,传值传参要先调用默认构造函数,在默认构造中实现深拷贝( _ a a a 指向的空间也拷贝一份),这样就没这些问题啦

  当然,对于自定义类型,函数传参是不建议用传值传参的。毕竟就算是正确拷贝,当拷贝的内容太大,也会占用很大空间,而且效率不高

  • 自定义类型传参,尽可能用引用,如果不改变,尽可能加 c o n s t const const

  

4.2.3、躺赢的 MyQueue

  当然,也不是所有有指向资源的类都需要自己写拷贝构造,比如 M y Q u e u e MyQueue MyQueue 类(用两个栈模拟实现队列)

// 两个Stack实现队列
class MyQueue
{public :
private:Stack pushst;Stack popst;
};
int main()
{MyQueue mq1;MyQueue mq2 = mq1;return 0;
}

在这里插入图片描述

   m q 2 mq2 mq2 是正常完成初始化的。
  因为对自定义类型成员,编译器会调用它自身的拷贝构造
  
  虽然MyQueue是躺赢,但这一切都是有 S t a c k Stack Stack 替他负重前行
  

4.2.4、传值返回与引用返回

  • 传值返回产生一个临时对象调用拷贝构造传引用返回,返回的是返回对象的别名(引用),没有产生拷贝

  
  什么意思呢?我们来看看

Stack func()
{Stack st;return st;
}int main()
{Stack ret = func();return 0;
}

  
  像上述代码,调用 f u n c func func 函数,使用传值返回。返回时,先调用拷贝构造 s t st st 拷贝到一个临时对象,后再调用拷贝构造将临时对象中的值拷贝到 ret 中。(实际编译器会进行优化,不会真的执行两个拷贝,但从语法层面来讲是会执行两次拷贝的)

在这里插入图片描述

  
  为了减少拷贝,我们会使用传引用返回,返回 s t st st 的别名

Stack& func()
{Stack st;return st;
}

  但是这是不对的,st 出函数作用域就销毁了,此时返回的是野引用,类似于野指针的东西

  使用引用返回,一定要确保返回的对象,当函数结束后还在,才能用引用返回

例如下面两种情况:

//情况一
Stack& func()
{static Stack st;return st;
}//情况二
Stack& func(Stack& st)
{st.push(1);st.push(1);st.push(1);return st;
}int main()
{Stack st1;Stack st2 = func(st1);return 0;
}

  
  

4.3、总结

  如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数
  
拷贝构造的特点:

  • 拷贝构造函数是构造函数的一个重载
  • 拷贝构造函数的第一个参数必须是自身类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发 无穷递归 。若有其他参数必须给缺省值
  • C++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参传值返回都会调用拷贝构造完成
  • 未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造
  • Date 这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显式实现拷贝构造。像 Stack 这样的类,虽然也都是内置类型,但是 _a 指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像 MyQueue 这样的类型内部主要是自定义类型 S t a c k Stack Stack 成员,编译器自动生成的拷贝构造会调用 S t a c k Stack Stack 的拷贝构造,也不需要我们显式实现 M y Q u e u e MyQueue MyQueue 的拷贝构造。
  • 这里有一个小技巧,如果一个类显式实现了析构函数并释放了资源,那么他就需要写拷贝构造,否则不需要
  • 传值返回产生一个临时对象调用拷贝构造传值引用返回,返回的对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回

  

5、取地址运算符重载

5.1、const 成员函数

当类对象被 c o n s t const const 修饰会发生什么呢?

class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;} void Print() {cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{const Date d(2024, 1, 1);d.Print();return 0;
}

  
  d.Print();这句代码,我们知道到调用成员函数要传递地址给 t h i s this this 指针的,这句代码实际上是d.Print(&d);
  但是 d d d类型const Date,因此 & d d d 传递的类型const Date*
  而 t h i s this this 指针类型Date* const this(这里 c o n s t const const 修饰的是指针本身,不是对象,可以直接忽略)
  

在这里插入图片描述

  很显然,const Date*传给Date*发生了权限放大,编译是无法通过的

  这时要把 t h i s this this指针 的类型变为const Date*
  怎么做呢?要知道 C++ 规定,我们是不能在形参的位置显式写this指针的,这样我们就不能直接通过形参来修改 t h i s this this 指针

  为此,C++就给了一个偏方:在函数参数列表后面加 c o n s t const const
如:

void Print() const
{cout << _year << "-" << _month << "-" << _day << endl;
}

  那如果对象是非 const 还能不能调用 P r i n t Print Print 呢?

int main()
{Date d1(2024, 7, 5);d1.Print();const Date d2(2024, 8, 5);d2.Print();return 0;
}

  可以的,权限虽然不能放大,但是能缩小

  所以,对于不用修改成员变量的成员函数,建议都加const,原因与引用加const类似

  

5.2、取地址运算符重载

  取地址运算符重载分为普通取地址运算符重载 c o n s t const const 取地址运算符重载

	Date* operator&(){return this;}const Date* operator&() const{return this;}

:两个都要写,因为普通对象返回Date* c o n s t const const 对象要返回const Date*

  取地址运算符重载也是一个默认成员函数,编译器会默认生成,往往不需要我们自己显式实现
  
  除非你不想让别人取到该对象地址,那你就可以这样写:

	Date* operator&(){return nullptr;}const Date* operator&() const{return nullptr;}

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

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

相关文章

世界500强排行榜公布 中国互联网企业表现突出

在2024年8月5日&#xff0c;《财富》杂志公布了最新的全球500强企业排行榜。 这些公司的总营收接近41万亿美元&#xff0c;占到了全球GDP的三分之一&#xff0c;其净利润同比增长2.3%&#xff0c;总计约2.97万亿美元。 中国有133家公司入选这一榜单&#xff0c;以11万亿美元的…

SpringMVC学习笔记---带你快速入门和复习

一、初识SpringMVC 1.1、什么是SpringMVC 1.1.1、什么是MVC MVC是一种软件架构模式&#xff08;是一种软件架构设计思想&#xff0c;不止Java开发中用到&#xff0c;其它语言也需要用到&#xff09;&#xff0c;它将应用分为三块&#xff1a; M&#xff1a;Model&#xff0…

数学建模--蒙特卡洛算法之电子管更换刀片寿命问题

目录 1.电子管问题重述 2.电子管问题分析 3.电子管问题求解 4.刀片问题重述 5.刀片问题分析 6.刀片问题求解 1.电子管问题重述 某设备上安装有4只型号规格完全相同的电子管&#xff0c;已知电子管寿命服从100&#xff5e;200h之间的均匀分布&#xff0e; 只要有一个电子管…

在线办公小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;通知公告管理&#xff0c;员工管理&#xff0c;部门信息管理&#xff0c;职位信息管理&#xff0c;会议记录管理&#xff0c;待办事项管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首…

Android经典实战之如何获取图片的经纬度以及如何根据经纬度获取对应的地点名称

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 在Android中&#xff0c;可以通过以下步骤获取图片的经纬度信息以及根据这些经纬度信息获取对应的地点名称。这里主要涉及两部分&#xff1a;从…

从0开始搭建vue + flask 旅游景点数据分析系统(七):可视化前后端对接实现

这一期继续编写flask后端&#xff0c;并且完成echarts折线图、柱状图和饼图的对接。 1 新增一些依赖 pip install Flask-SQLAlchemy Flask-Marshmallow pymysql修改 init.py文件&#xff0c;下面给出完整代码&#xff1a; from flask import Flask from flask_sqlalchemy im…

leetcode70_爬楼梯

思路 动态规划 爬到第n阶楼梯的方法数为&#xff1a;第n-1阶楼梯的方法数 第n-2阶楼梯的方法数 func climbStairs(n int) int {if n < 2 {return 1}dp : make([]int, n1)dp[1] 1dp[2] 2for i:3; i<n; i {dp[i] dp[i-1] dp[i-2]}return dp[n] }

Kubernetes中的PV)和 PVC深度剖析

在容器化的世界里&#xff0c;持久化存储一直是一个重要且复杂的问题。Kubernetes&#xff08;以下简称K8s&#xff09;为了解决容器中的数据持久化问题&#xff0c;提出了Persistent Volume&#xff08;PV&#xff09;和Persistent Volume Claim&#xff08;PVC&#xff09;这…

大数据信用报告查询哪家平台的比较好?

相信在搜索大数据信用的你&#xff0c;已经因为大数据信用不好受到了挫折&#xff0c;想详细了解一下自己的大数据信用&#xff0c;但是找遍了网络上的平台之后才发现&#xff0c;很多平台都只提供查询服务&#xff0c;想要找一个专业的平台查询和讲解很困难。下面本文就为大家…

LeetCode 150.逆波兰表达式求值

LeetCode 150.逆波兰表达式求值 思路&#x1f9d0;&#xff1a; 用栈存储该字符串&#xff0c;如果遇到数字就入栈&#xff0c;遇到符号就将数字出栈计算后再入栈&#xff0c;当整个字符串遍历完后&#xff0c;栈顶值就是该表达式的值。 代码&#x1f50e;&#xff1a; class …

【OpenCV C++20 学习笔记】范围阈值操作

范围阈值操作 原理HSV颜色空间RGB与HSV颜色空间之间的转换 代码实现颜色空间的转换范围阈值操作 原理 HSV颜色空间 HSV(色相hue, 饱和度sarturation, 色明度value)颜色空间与RGB颜色空间相似。hue色相通道代表颜色类型&#xff1b;saturation饱和度通道代表颜色的饱和度&…

MySQL-MHA高可用配置及故障切换

目录 案例搭建 1&#xff1a;所有服务器关闭防火墙 2&#xff1a;设置hosts文件 3&#xff1a;安装 MySQL 数据库 4&#xff1a;修改参数 5&#xff1a;安装 MHA 软件 6&#xff1a;配置无密码认证 7&#xff1a;配置 MHA 8&#xff1a;模拟 master 故障 MHA(MasterHi…

【Python修改所有可执行程序的图标】

孩子还小&#xff0c;不懂事写着玩的 警告&#xff1a;请谨慎使用。该程序会修改全系统所有可执行文件图标(其实就是注册表)&#xff0c;在重新开机后生效 演示&#xff1a; 把应用程序图标改为记事本&#x1f5d2; 原理&#xff1a; Windows 操作系统通过注册表来存储和管…

不懂期权怎么交易?看这个例子就懂了

期权就是股票&#xff0c;唯一区别标的物上证指数&#xff0c;会看大盘吧&#xff0c;期权交易两个方向认购做多&#xff0c;认沽做空&#xff0c;双向t0交易没了&#xff0c;跟期货一样&#xff0c;对的&#xff0c;玩的也是合约&#xff0c;唯一区别没有保证金不会爆仓&#…

CAD二次开发IFoxCAD框架系列(15)- IFox的介绍和初始化

背景介绍 因为本人在光伏行业从事软件研发&#xff0c;最近我一直在做CAD方面技术的预研和探索。在研究CAD的SDK的时候&#xff0c;发现很多写法比较繁琐&#xff0c;所以一直想封装工具&#xff0c;提高开发效率&#xff0c;也做了很多的CAD工具的封装&#xff0c;大家可以看…

一文读懂如何选择视频孪生三维建模方式及建模精度等级

导言/INTRODUCTION 三维模型是视频孪生应用的基础&#xff0c;建模方式与模型精度将直接影响到最终孪生场景的呈现和应用效果。各种建模方式和模型精度在成本、场景还原真实度、实施周期方面都有自己的特点&#xff0c;因而有着各自的优劣势和适用场景&#xff0c;同一场景可能…

鸿蒙开发5.0【应用异常处理】运维

应用异常处理 介绍 本示例介绍了通过应用事件打点hiAppEvent获取上一次应用异常信息的方法&#xff0c;主要分为应用崩溃、应用卡死两种。 效果图预览 使用说明 点击构建应用崩溃事件&#xff0c;3s之后应用退出&#xff0c;然后打开应用进入应用异常页面&#xff0c;隔1mi…

pdf怎么加密码怎么设置密码?pdf加密码的几种设置方法

在数字化时代&#xff0c;信息的保密性与安全性日益成为我们不可忽视的重要环节。尤其对于包含敏感信息或个人隐私的PDF文档而言&#xff0c;保护其免受未授权访问的侵扰显得尤为重要。通过为PDF文档设置密码保护&#xff0c;我们能够筑起一道坚实的防线&#xff0c;确保只有拥…

Java常见面试题-13-FastDFS

文章目录 FastDFS 是什么&#xff1f;FastDFS 组成FastDFS 的流程FastDFS 如何现在组内的多个 storage server 的数据同步&#xff1f; FastDFS 是什么&#xff1f; FastDFS 是一个开源的轻量级分布式文件系统&#xff0c;它可以对文件进行管理&#xff0c;功能包括&#xff1…

创意指南丨VR游览沉浸式空间体验

欢迎来到我们制作的VR幻想世界。玩家的起点是一条蓝色水晶大道&#xff0c;让我们一起探索这个如梦似幻的境地。 在这条大道的两侧&#xff0c;漂浮着半透明的大水晶水母。它们轻盈地在空中飘动&#xff0c;仿佛在欢迎我们的到来。这条道路上方&#xff0c;一个个半圆环不停地…