当前位置: 首页 > news >正文

类和对象(上续)

前言:本文介绍类和对象中的一些比较重要的知识点,为以后的继续学习打好基础。

目录

拷贝构造

拷贝构造的特征:

自定义类型的传值传参

自定义类型在函数中的传值返回 

如果返回值时自定义的引用呢?

在什么情况下使用呢?

拷贝构造中的浅拷贝问题

 ​编辑

为什么会释放两次呢?

那么什么情况下需要深拷贝?

运算符重载 

运算符重载的基本语法:

类中运算符重载函数的调用的两种方法:

运算符重载与函数重载

运算符重载的特征:

运算符重载的价值:

如果将运算符重载成全局函数,就无法访问类中的私有成员了。

解决方法:

赋值运算符

调用拷贝构造与调用赋值重载的区别


拷贝构造

拷贝构造是一种特殊的构造函数

拷贝构造的特征:

1.是构造函数的重载

2.参数只有一个并且只能是引用

3.拷贝构造可以不显示写,编译器会自动生成默认构造。

浅拷贝(值拷贝)就是一个字节一个字节的拷贝。

编译器自动生成的默认拷贝的特点:对内置类型的成员,浅拷贝(值拷贝);对自定义类型的成员,拷贝需要调用其拷贝构造

#include <iostream>
using namespace std;
class Date
{
public:Date(int year,int month,int day){_year = year;_month = month;_day = day;}//拷贝构造Date(Date& d){cout << "拷贝构造" << endl;_year = d._year;_month = d._month;_day = d._day;}
private:int _year;int _month;int _day;
};
int main()
{Date d1(2024,6,10);Date d2(d1);//拷贝构造Date d3 = d2;//拷贝构造return 0;
}

自定义类型的传值传参

#include <iostream>
using namespace std;
class Date
{
public:int GetYear(){return _year;}int GetMonth(){return _month;}int GetDay(){return _day;}Date(int year,int month,int day){_year = year;_month = month;_day = day;}Date(Date& d){cout << "拷贝构造" << endl;_year = d._year;_month = d._month;_day = d._day;}
private:int _year;int _month;int _day;
};void f(Date d)
{cout << d.GetYear() << " " << d.GetMonth() << " " << d.GetDay() << endl;
}
int main()
{Date d1(2024,6,10);Date d2(d1);Date d3 = d2;f(d1);return 0;
}

以上代码的运行结果 

在给f函数传值传参时,调用了一次拷贝构造。

结论:自定义类型在进行传值传参时会进行拷贝构造。

如果拷贝构造的参数是自定义类型不是自定义的引用那么就会出现无穷递归调用

自定义类型在函数中的传值返回 

下面有一段代码,以这段代码为例讲一下该问题。

#include <iostream>
using namespace std;class Date
{public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};Date f(Date d)
{return d;
}int main()
{Date d1(2024, 6, 10);Date d2 = f(d1);return 0;
}

 

 

结论:如果返回值,是自定义类型,那么,返回时就会进行拷贝构造,创建临时对象,再将临时对象赋值给正在创建的类

用一段错误代码解释上面的结论:

Date f(Date& d)
{return d;
}int main()
{Date d1(2024, 6, 10);Date& d2 = f(d1);return 0;
}

 

上述错误代码的报错: 

 

原因是因为,临时对象具有常性,用d2来引用临时对象是会出现权限放大的问题,所以验证了上述的结论,如果加上const(权限平移)报错就会消失。 

而编译器为了提高效率往往会直接将其优化为一次拷贝构造

如果返回值时自定义的引用呢?

Date& f()
{Date d1(2023, 1, 2);return d1;
}int main()
{Date& d1 = f();return 0;
}

 因为,d1 实在函数中定义的对象,出了函数的作用域就会销毁。

栈帧的角度来理解,引用的本质是指针,f函数被销毁了,main函数中的d1仍指向f中的d1的那块已被销毁的空间

自定义类型的引用返回存在风险

在什么情况下使用呢?

出了函数的作用域生命周期没到,不构析对象还在,,那么就可以用引用 返回

出了函数的作用域生命周期到了,析构,对象不存在,那么就只能用传值返回

 

拷贝构造中的浅拷贝问题

以下代码存在浅拷贝问题

#include <stdlib.h>
#include <iostream>
using namespace std;
class Stack
{
public:Stack(int capacity = 4){cout << "Stack()" << endl;_arr = (int*)malloc(sizeof(int) * capacity);_capacity = capacity;_top = 0;}~Stack(){cout << "~Stack()"<<endl;free(_arr);_capacity = 0;_top = 0;}
private:int* _arr;int _top;int _capacity;
};int main()
{Stack st1(4);Stack st2(st1);return 0;
}

程序崩溃:

 

 

该代码的问题就在于对一块开辟的空间释放两次

为什么会释放两次呢?

因为没有显示写拷贝构造函数,所以用的是编译器自动生成的拷贝构造函数(浅拷贝),所以在拷贝构造st2时,使st2中_arr指向的空间与st1中的一样,最后分别调用析构函数时,就造成了对用一块开辟的空间释放两次。 

解决方案就是深拷贝

	Stack(Stack& st){_arr = (int*)malloc(sizeof(int) * st._capacity);//深拷贝_capacity = st._capacity;_top = st._top;}

那么什么情况下需要深拷贝?

总结:

1.如果没有管理资源,就不显示写拷贝构造,用默认拷贝构造就可以

2.都是自定义的成员,内置类型(内置类型不指向资源),也用默认拷贝;如果自定义类型的成员的内置类型指向资源,那么在该自定义类型中显示写拷贝构造

3.一般,不需要写析构函数,就需要写构造函数

4.内部有指针或一些值指向资源,显示写析构释放,通常需要写拷贝构造来完成深拷贝

运算符重载 

 

运算符重载的基本语法:

返回值类型 + operater+运算符(参数列表)

operator是关键字,operator和运算符一起构成函数名。

类中运算符重载函数的调用的两种方法:

    //在类中实现的+运算符重载(实现日期与天数的相加)Date operator+(int day){_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);_month++;if (_month > 12){_month = 1;_year++;}}return *this;}  //已经在类中写了一个加号重载的函数Date d1(2024, 6, 10);d1 + 100;//第一种调用方法d1.operator+(100);//第二种调用方法

运算符重载与函数重载

运算符重载和函数重载没有关系,是两回事,多个相同的运算符的重载是函数重载

比如<<(流插入)可以自动识别内置类型的原因就是对<<进行重载,构成了函数重载。

运算符重载的特征:

1.不能通过其他符号重载

2.必须有一个类类型的参数

3.含义不能改变(这里是建议,比如重载的+的含义是将两个数相加,而你写的含义是相减)

4.一般,参数比运算符操纵的操作数的数目少1,因为在参数列表中有隐含的this指针

5. .*    ::    sizeof   ?:   .   这五个操作符不能被重载,.*是用于类成员函数指针的访问,

如果想了解:函数指针到底需不需要解引用?类成员函数呢?_函数指针需要解引用吗-CSDN博客

   

运算符重载的价值:

运算符重载是运算符不仅限于操纵内置类型的数据,可以实现类与类之间,或类与内置类型直间的运算,可以增强代码的可读性

一个使用运算符重载的例子:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}//得到当前月份的天数int	GetMonthDay(int year, int month){int month_day[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };if(month==2&&((year%4==0&&year%100!=0)||year%400==0))return 29;return month_day[month];}//重载+运算符实现日期与天数的相加Date operator+(int day){_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);_month++;if (_month > 12){_month = 1;_year++;}}return *this;}void Print(){cout << _year << " " << _month << " " << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1(2024, 6, 10);Date d2 = d1 + 100;d2.Print();return 0;
}

上述代码中的+的重载,是在类中实现的,或在类中声明,在类外实现。

如果将运算符重载成全局函数,就无法访问类中的私有成员了。

解决方法:

1.在类中实现成员的Get(获取成员)和Set(重新给成员赋值)的接口

2.将全局函数设为该类的友元

3.重载为成员函数(可以访问类的成员,但函数不在是全局函数)

这些方法比较建议第二种。

以下的代码是通过友元来实现全局减号的运算符重载

#include <iostream>
using namespace std;
class Date
{//友元就是在函数前加上一个关键字friend,并在相应的类中声明friend Date operator-(Date& d,int day);
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}int	GetMonthDay(int year, int month){int month_day[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };if(month==2&&((year%4==0&&year%100!=0)||year%400==0))return 29;return month_day[month];}Date operator+(int day){_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);_month++;if (_month > 12){_month = 1;_year++;}}return *this;}void Print(){cout << _year << " " << _month << " " << _day << endl;}
private:int _year;int _month;int _day;
};//全局函数减号的重载
Date operator-(Date& d,int day)
{d._day -= day;while (d._day <= 0){if (d._month == 1){d._month = 12;d._year--;}else{d._month--;}d._day += d.GetMonthDay(d._year, d._month);}return d;
}



赋值运算符

赋值运算符重载也是6个默认成员函数之一

调用拷贝构造与调用赋值重载的区别

	Date d1(2024, 6, 10);Date d2 = d1;//拷贝构造Date d3(d1);//拷贝构造Date d4(2024, 2, 11);d4 = d1;//赋值重载

注意:上面代码中两个等号的调用方式容易混,但最后这两个有本质的区别。

结语:希望本文能够让你有所收获 。

相关文章:

  • 力扣 T62 不同路径
  • leetcode389:找不同
  • XUbuntu24.04之制作ISO镜像启动盘(二百四十八)
  • module ‘django_cas_ng.views‘ has no attribute ‘login‘
  • 备战 清华大学 上机编程考试-冲刺前50%,倒数第5天
  • VM渗透系统合集(下载链接)
  • Objective-C的初始化方法中,应该如何读写属性
  • svnadmin备份和还原
  • 大模型训练的艺术:从预训练到增强学习的四阶段之旅
  • 数字IC必备知识点:【0】文章汇总
  • 爱德华三坐标软件ACdmis.AC-dmis密码注册机
  • 大模型开发Semantic Kernel 简介
  • java版多语言抢单系统 多语言海外AEON抢单可连单加额外单源码 抢单平台搭建开发 抢单开挂的软件
  • Linux shell编程学习笔记58:cat /proc/mem 获取系统内存信息
  • MySQL-数据处理(1)
  • @jsonView过滤属性
  • 【附node操作实例】redis简明入门系列—字符串类型
  • Angular数据绑定机制
  • JavaScript工作原理(五):深入了解WebSockets,HTTP/2和SSE,以及如何选择
  • Java新版本的开发已正式进入轨道,版本号18.3
  • node 版本过低
  • Objective-C 中关联引用的概念
  • Odoo domain写法及运用
  • python3 使用 asyncio 代替线程
  • spark本地环境的搭建到运行第一个spark程序
  • SpringCloud集成分布式事务LCN (一)
  • Storybook 5.0正式发布:有史以来变化最大的版本\n
  • TiDB 源码阅读系列文章(十)Chunk 和执行框架简介
  • Wamp集成环境 添加PHP的新版本
  • 缓存与缓冲
  • 解析带emoji和链接的聊天系统消息
  • 数组大概知多少
  • 王永庆:技术创新改变教育未来
  • # Python csv、xlsx、json、二进制(MP3) 文件读写基本使用
  • #pragma once
  • #常见电池型号介绍 常见电池尺寸是多少【详解】
  • $GOPATH/go.mod exists but should not goland
  • $refs 、$nextTic、动态组件、name的使用
  • (C语言)二分查找 超详细
  • (iPhone/iPad开发)在UIWebView中自定义菜单栏
  • (阿里云在线播放)基于SpringBoot+Vue前后端分离的在线教育平台项目
  • (动手学习深度学习)第13章 计算机视觉---图像增广与微调
  • (附源码)springboot太原学院贫困生申请管理系统 毕业设计 101517
  • (附源码)ssm教师工作量核算统计系统 毕业设计 162307
  • (十八)Flink CEP 详解
  • (十七)devops持续集成开发——使用jenkins流水线pipeline方式发布一个微服务项目
  • (四)图像的%2线性拉伸
  • (原创)攻击方式学习之(4) - 拒绝服务(DOS/DDOS/DRDOS)
  • (转)Scala的“=”符号简介
  • *上位机的定义
  • *算法训练(leetcode)第四十五天 | 101. 孤岛的总面积、102. 沉没孤岛、103. 水流问题、104. 建造最大岛屿
  • .net framework profiles /.net framework 配置
  • .NET 表达式计算:Expression Evaluator
  • .NET 实现 NTFS 文件系统的硬链接 mklink /J(Junction)
  • .NET单元测试使用AutoFixture按需填充的方法总结