[C++] 类和对象 _ 剖析构造、析构与拷贝


一、构造函数

构造函数是特殊的成员函数,它在创建对象时自动调用。其主要作用是初始化对象的成员变量(不是开辟空间)。构造函数的名字必须与类名相同,且没有返回类型(即使是void也不行)。

在C++中,构造函数是专门用于初始化对象的方法。当创建类的新实例时,构造函数会自动被调用。通过构造函数,我们可以确保对象在创建时就被赋予合适的初始状态。下面我将详细解释如何使用构造函数进行初始化操作,并以Date类为例进行说明。

创建一个Date类:

class Date 
{  
public:  // 成员函数...  
private:  int _year;  int _month;  int _day;  
};

构造函数的特征

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。

无参构造

无参构造函数允许我们创建Date对象而不提供任何参数。但是,需要注意的是,如果我们不在无参构造函数中初始化成员变量,那么这些变量的初始值将是未定义的,这可能会导致程序出错。
Date d1; // 调用无参构造函数

class Date 
{  
public:  // 1. 无参构造函数  Date() {  // 在这里可以添加一些初始化代码,例如设置默认日期  // 例如:_year = 2000; _month = 1; _day = 1;  }  // 其他成员函数...  private:  int _year;  int _month;  int _day;  
};

带参构造

带参构造可以和无参构造函数重载,因为在之后调用的时候不会受影响,可以与之后讲解的全缺省构造函数和无参构造函数之间的不能函数重载的进行区别。

带参构造函数可以在对对象进行初始化的时候进行传参,传参的数值会直接进行初始化对象中的成员变量。
Date date2(2023, 3, 15); // 调用带参构造函数创建对象,并初始化日期为2023年3月15日

class Date 
{  
public:  // 1. 无参构造函数  Date() {  // ...  }  // 2. 带参构造函数  Date(int year, int month, int day) {  _year = year;  _month = month;  _day = day;  }  // 其他成员函数...  private:  int _year;  int _month;  int _day;  
};

在这个带参构造函数中,我们通过参数year、month和day来初始化_year、_month和_day成员变量。这样,我们就可以在创建Date对象时直接指定日期了。

注意区别创造对象的格式

Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数

默认无参构造函数

参考代码:

class Date
{
public:
/*
// 如果用户显式定义了构造函数,编译器将不再生成
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
*/void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{// 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用Date d1;return 0;
}

在C++中,如果你没有为类显式定义任何构造函数,编译器会为你自动生成一个默认的无参构造函数。这个默认构造函数不会执行任何操作,也不会初始化类的成员变量。这意味着,如果你的类Date没有显式定义任何构造函数,那么你可以创建一个Date对象而不提供任何参数,编译器会为你调用这个默认构造函数。
然而,一旦你为类显式定义了任何构造函数(无论是带参还是无参),编译器就不会再自动生成默认构造函数了。因此,如果你屏蔽了Date类中的带参构造函数,编译器会为你生成一个默认构造函数,所以你可以直接这样创建对象:

Date d1;

但是,当你放开带参构造函数时,由于你已经显式定义了至少一个构造函数,编译器就不会再为你生成默认构造函数了。因此,在尝试这样创建对象时,编译器会报错,因为它找不到一个合适的默认构造函数来调用。错误信息表明编译器找不到一个可以调用的构造函数,因为没有默认构造函数可用。

不显式定义构造函数(系统默认生成)

请注意:
默认构造函数只对自定义类型进行初始化,内置类型不做处理。
但是自定义类型的最终还是要对自定义类型中的内置类型进行初始化,所以要在类创建的时候就做好处理。

问题的解决方式

问题描述:
显式定义构造函数的影响:一旦你为类显式定义了至少一个构造函数(无论带参还是不带参),编译器就不会再自动生成默认构造函数。这意味着如果你想要创建类的对象而不提供任何参数,你必须自己定义一个无参构造函数,否则编译器会报错,因为它找不到一个合适的构造函数来调用。

显式定义的无参构造函数
class Date 
{
public:// 显式定义的无参构造函数  Date() {_year = 0;_month = 0;_day = 0;}// 其他成员函数...  private:int _year;int _month;int _day;
};

带参构造函数
// 带参构造函数  Date(int year, int month, int day) {  _year = year;  _month = month;  _day = day;  }

全缺省参数的构造函数

C++11 😗*内置类型成员变量在类中声明时可以给默认值。 **

使用全缺省参数即可解决5.2问题,在该小节中主要对全缺省参数的构造函数进行详细讲解。
全缺省参数的构造函数结构类似于以下代码:

Date(int year = 1900, int month = 1, int day = 1)  
{  _year = year;  _month = month;  _day = day;  
}

特点:会在参数列表中进行类似于赋值的操作
这个构造函数接受三个参数,并且每个参数都有一个默认值。这意味着,在创建Date对象时,你可以选择性地提供这些参数。如果你没有为任何一个参数提供值,那么它们将使用默认值(即1900年1月1日)。

可以思考以下代码在创建对象的时候会不会编译通过:

class Date
{
public:Date(){_year = 1900;_month = 1;_day = 1;}Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};

结论是:无法通过。
原因是:
语法可以存在、调用存在歧义。
无参构造和全缺省存在歧义,当使用不传参创建对象Date d;的时候编译器无法抉择选择构造函数。

推荐使用全缺省参数的构造函数。

二、析构函数

析构函数是一种特殊的成员函数,它在对象的生命周期结束时自动被调用。其主要职责是执行与对象销毁相关的清理操作,如释放动态分配的内存、关闭文件等。

对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

特性

  1. 析构函数名是在类名前面加上“ ~ ”
  2. 无参数和返回值

~Stack() { }

  1. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构

函数不能重载

  1. 对象生命周期结束时,C++编译系统系统自动调用析构函数

用栈来理解析构函数

typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (nullptr == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){if (_size == _capacity){// 扩展数组大小_capacity *= 2;_array = (DataType*)realloc(_array, sizeof(DataType) * _capacity);if (nullptr == _array){perror("realloc扩展空间失败!!!");return;}}_array[_size] = data;_size++;}// 其他方法...~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType* _array;size_t _capacity;size_t _size;
};
void TestStack()
{Stack s;s.Push(1);s.Push(2);
}int main() 
{TestStack();return 0;
}

析构函数的析构过程解析

当正确使用析构函数后就不用担心程序中有内存泄漏的情况了,因为在每次该对象生命周期结束后都会自动调用析构函数,流程如下:
①准备出生命周期
image.png
②出生命周期,进入析构函数
image.png
③析构函数执行完毕,对象销毁
image.png

编译器自动生成构造函数

特性
  1. 内置类型不做处理
  2. 自定义类型会去调用它的析构函数

以Leetcode 用栈实现队列该题为例:https://leetcode.cn/problems/implement-queue-using-stacks/description/ ,讲解编译器自动生成的构造函数的特性。

该题思路为:将一个栈当作输入栈,用于压入 push 传入的数据;另一个栈当作输出栈,用于 pop 和 peek操作。

将流程简化为:

class MyQueue
{
private:Stack _pushst;Stack _popst;
};

该类中成员变量只有两个自定义类型Stack,所以在析构自定义类型的时候会去调用Stack类的析构函数

~Stack()
{if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}
}

从而将Stack类中的动态申请的资源给释放掉,以避免内存泄漏。

结论
  1. 自定义类的销毁的最终还是需要将动态申请的资源清理,所以一般情况下,有动态申请资源,就需要写析构函数释放资源,因为编译器自动生成的析构函数最终还是无法释放动态申请的资源,只是深入的去调用当前类中自定义类型的析构函数。
  2. 没有懂太申请的资源,不需要写析构函数
  3. 需要释放资源的成员都是自定义类型,不用写析构。

三、拷贝构造函数

什么是拷贝构造?

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用.

特性

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
    因为会引发无穷递归调用。

如何定义和使用拷贝构造函数

定义

浅拷贝

浅拷贝只是简单地复制对象的成员变量值,包括指针成员的地址,而不是复制指针所指向的内容。这可能会导致多个对象共享同一个内存地址,当一个对象修改了内存中的内容时,其他对象也会受到影响。

ShallowCopy(const ShallowCopy& other)
{data = other.data;
}

深拷贝

深拷贝则是在拷贝对象时,复制指针所指向的内容,而不是简单地复制地址。这样每个对象都拥有自己的内存空间,互相之间不会受到影响。

DeepCopy(const DeepCopy& other) 
{data = new int;*data = *(other.data);
}

拷贝构造函数的使用

代码

以深拷贝为例写一个完整的拷贝构造函数的使用代码:

#include <iostream>class DeepCopy 
{
private:int *data;
public:// 构造函数DeepCopy(int value) {data = new int;*data = value;}// 拷贝构造函数(深拷贝)DeepCopy(const DeepCopy& other) {data = new int;*data = *(other.data);}// 获取数据的函数int getData() const {return *data;}// 设置数据的函数void setData(int value) {*data = value;}// 析构函数~DeepCopy() {delete data;}
};int main() 
{DeepCopy obj1(10);DeepCopy obj2 = obj1;// 修改obj1的数据obj1.setData(20);std::cout << "obj1的数据:" << obj1.getData() << std::endl;std::cout << "obj2的数据:" << obj2.getData() << std::endl;return 0;
}

注意:防止无限循环
#include <iostream>class MyClass 
{
private:int data;
public:// 拷贝构造函数MyClass(const MyClass other) {// 构造信息}
};int main() 
{MyClass obj;MyClass newObj = obj; // 这里会调用拷贝构造函数return 0;
}

当在main函数中进行拷贝构造的时候调用的拷贝构造函数是:

MyClass(const MyClass other) 
{// 构造信息
}

在使用该拷贝构造函数进行拷贝构造的时候就会出现无限循环拷贝,因为形参为MyClass other而不是MyClass& other,为什么出现这样的情况呢?
可以思考。在main函数中拷贝传参的时候 MyClass newObj = obj相当于将obj作为参数传入拷贝构造函数,其在main中对应格式为类 = 类所以调用了拷贝构造。而在拷贝构造函数中呢,也相当于类(形参) = 类(实参),这样不也相当于拷贝构造吗?所以也会进行调用拷贝构造函数,如此下来,就陷入了拷贝构造函数的无限循环调用。

所以我们在使用拷贝构造函数的时候要注意避免陷入无限循环:

  1. 形参使用引用方式
  2. 不在拷贝构造内进行拷贝构造

默认拷贝构造函数

当你没有显式地为类定义一个拷贝构造函数时,C++编译器会自动生成一个默认的拷贝构造函数。默认的拷贝构造函数执行的是浅拷贝,即简单地将每个成员变量的值从原始对象复制到新对象中。

在一些情况下默认的拷贝构造函数会有危害:
当类中存在指针成员时,编译器默认的拷贝构造函数只会复制指针的值,而不会复制指针所指向的内容。这就意味着,如果两个对象共享同一个资源,例如动态分配的内存,那么在其中一个对象销毁时,会释放相同的内存地址,导致另一个对象访问到无效的内存。这种情况下,就需要我们自己来手动编写拷贝构造函数来执行深拷贝,以确保每个对象都有自己的资源副本。
所以当类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

函数返回值类型为类类型对象

可以思考如下代码:

// 1.
Stack& func()
{Stack st;return st;
}// 2. 
Stack func()
{Stack st;return st;
}// 3. 
Stack& func()
{static Stack st;return st;
}

分析①

// 1.
Stack& func()
{Stack st;return st;
}

该程序的结果是:崩溃

该函数返回值使用类引用进行返回,在函数中用直接创建了一个对象然后进行返回。
为什么会崩溃呢?
在函数中创建了一个对象并进行返回,但是在函数结束后也就出了st的域,所以会调用Stack的析构函数对st进行析构,从而导致之前返回的那个值变为了析构后的结果,然后在返回的那个值出了它的域之后又会进行一次析构,这时候析构的就是已经析构过的对象了,所以会进行崩溃。

分析②

// 2. 
Stack func()
{Stack st;return st;
}

②与①进行对比,没有返回对象的引用,所以程序可以正常运行,

这个函数返回一个Stack对象。在函数结束时,局部对象st会被销毁,但返回的是一个副本,因此不会直接导致访问无效内存的问题。
后面的操作取决于该类的拷贝构造函数。

分析③

// 3. 
Stack& func()
{static Stack st;return st;
}

这个函数返回一个静态局部对象的引用。静态局部对象在函数结束时不会被销毁,因此返回的引用仍然是有效的。


Black and White Gamer _Hacks or Reviews_ Gaming YouTube Video Intro.png

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

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

相关文章

【Canvas技法】流星雨的实现

【关键点】 流星的绘制&#xff0c;本质上还是绘制一条直线&#xff0c;但在渲染上有差别。 通常绘制直线都是给的固定颜色&#xff0c;绘制流星给的是渐变色&#xff0c;渐变色的开头是与背景色对比度明显的亮色&#xff0c;结尾是与背景色相同的暗色&#xff0c;中间渐变过…

Vue---router实现路由跳转

Vue—router实现路由跳转 目录 Vue---router实现路由跳转基本使用路由跳转html实现路由跳转JS实现路由跳转 基本使用 所谓路由&#xff0c;就是将一个个组件映射到不同的路由url中 首先要将App内的内容换成router-view // App.vue <template><div id"app"…

区间预测 | PSO-RF-KDE的粒子群优化随机森林结合核密度估计多变量回归区间预测(Matlab)

区间预测 | PSO-RF-KDE的粒子群优化随机森林结合核密度估计多变量回归区间预测&#xff08;Matlab&#xff09; 目录 区间预测 | PSO-RF-KDE的粒子群优化随机森林结合核密度估计多变量回归区间预测&#xff08;Matlab&#xff09;效果一览基本介绍程序设计参考资料 效果一览 基…

巧用 TiCDC Syncpiont 构建银行实时交易和准实时计算一体化架构

本文阐述了某商业银行如何利用 TiCDC Syncpoint 功能&#xff0c;在 TiDB 平台上构建一个既能处理实时交易又能进行准实时计算的一体化架构&#xff0c;用以优化其零售资格业务系统的实践。通过迁移到 TiDB 并巧妙应用 Syncpoint&#xff0c;该银行成功解决了原有多个 MySQL 集…

图搜索算法详解与示例代码

在计算机科学领域&#xff0c;图搜索算法是一类用于在图数据结构中查找特定节点或路径的算法。图搜索算法在许多领域都有着广泛的应用&#xff0c;包括网络路由、社交网络分析、游戏开发等。本文将详细介绍几种常见的图搜索算法&#xff0c;包括深度优先搜索&#xff08;DFS&am…

模方试用版水面修整,调整水岸线功能进程缓慢该怎么解决?

答&#xff1a;水面修整&#xff0c;第一个点选取准确的高程位置和水边&#xff0c;其他点就可以包含整个水面范围就行&#xff0c;可以绘制大一些。上图绘制区域没有包含到所有的水面&#xff0c;可以尝试下图的红线绘制区域。 模方是一款针对实景三维模型的冗余碎片、水面残缺…

【Spring 】Spring MVC 入门Ⅱ

Spring MVC 入门Ⅱ 一、接收Cookie / Session 这两者都是用来保存用户信息的&#xff0c;但不同的是&#xff1a; Cookie存在客户端 Session存在服务器 Session产生时会生成一个唯一性的SessionID&#xff0c;这个SessionID可以用于匹配Session和Cookie SessionID可以在Cooki…

模型训练中的过拟合和欠拟合

基本概念 我们知道&#xff0c;所谓的神经网络其实就是一个复杂的非线性函数&#xff0c;网络越深&#xff0c;这个函数就越复杂&#xff0c;相应的表达能力也就越强&#xff0c;神经网络的训练则是一个拟合的过程。   当模型的复杂度小于真实数据的复杂度&#xff0c;模型表…

python中的进程线程和协程

目录 进程&#xff08;Process&#xff09;多进程代码实例 线程&#xff08;Thread&#xff09;多线程存在原因及其缺点多线程代码实例 协程&#xff08;Coroutine&#xff09;协程的优点协程代码实例 进程、线程和协程适合的任务性质和环境多进程更适合的场景多线程更适合的场…

在Android中,如何通过Kotlin协程处理多个API调用

在Android中&#xff0c;如何通过Kotlin协程处理多个API调用 在Android开发中&#xff0c;如何使用Kotlin协程处理多个API调用的示例呢&#xff1f;假设我们已经对Kotlin协程有了一定的了解&#xff0c;包括定义、简单用例和示例等。现在&#xff0c;让我们来看一些真实的Andr…

Tokitsukaze and Average of Substring

原题链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 目录 1. 题目描述 2. 思路分析 3. 代码实现 1. 题目描述 2. 思路分析 前缀和。 开一个int类型的前缀和数组pre[30][N]&#xff08;pre[i][j]表示某字符转成的数字 i 在一段区间的前缀个数。因为字母表有‘a’~z…

带你学C语言:结构体及其内存

目录 &#x1f37a;0.前言 ✍1.结构体 &#x1f440;1.1为何结构体 &#x1f440;1.2结构体怎么声明 &#x1f440;1.3结构体怎么创建 &#x1f440;1.4结构体初始化与访问 ✋1.5匿名结构体问题 &#x1f646;1.6结构体的自我调用 &#x1f69d; 2.结构体的内存对齐 &a…

【数据结构】时间复杂度和空间复杂度解析

数据结构前言&#xff1a; 1. 什么是数据结构 打个比方来说不同的数据就相当于不同的书籍&#xff0c;我们经常在图书馆可以看到不同类别的书籍会被整理放在书架上方便查看存放&#xff0c;数据结构就是一种计算机存储管理数据的方式。 2. 什么是算法 算法就是一系列的计算…

UDP和TCP(传输层)

这里写目录标题 UDPUDP的基本特点UDP协议报文格式 TCPTCP协议报文格式TCP特点可靠传输实现机制确认应答超时重传数据丢了应答报文丢了 小结 UDP UDP的基本特点 无连接不可靠传输面向数据报全双工 UDP协议报文格式 2个字节有效范围(无符号): 0 ~ 65535(2^16 - 1). 2个字节有效范…

安装 AngularJS

安装 AngularJS 文章目录 安装 AngularJS1. 使用在线 cdn2. 使用依赖管理工具 npm 1. 使用在线 cdn <!-- 1. 引入在线地址 --> <script src"http://code.angularjs.org/1.2.25/angular.min.js"></script><!-- 2. 下载到本地&#xff0c;引入文…

【Python】常用数据结构

1、熟悉字典和列表 2、使用条件判断语句 3、list列表中计算 1、从键盘输人一个正整数列表,以-1结束,分别计算列表中奇数和偶数的和。 &#xff08;1&#xff09;源代码&#xff1a; # 初始化奇数和偶数的和为0 odd_sum 0 even_sum 0 #输入 while True:num int(input(&qu…

ubuntu下安装配置python3.11

方案1 添加仓库&#xff1a; $ sudo add-apt-repository ppa:deadsnakes/ppa $ sudo apt update $ sudo apt install python3.11然后查看有多少个python版本已经安装了&#xff1a; ls -l /usr/bin/python*python2.7,python 3.8 ,python 3.11. 然后&#xff0c;设置系统默认…

智能车入门——‘教程引导’ <新手从零做车>

目录 前言 本系列文章是为了帮助第一次接触智能车或者学校没有传承&#xff0c;不知道如何上手做智能车的同学。 通过阅读完整个系列&#xff0c;你应该能够制作一辆正常参赛的智能车。 我写这一系列博客的初衷主要是为了方便新手快速入门智能车。 如果追求高级算法以及提速&a…

Q1季度家用健身器械行业线上市场销售数据分析

自疫情开始&#xff0c;全民健身的浪潮就持续至今。然而&#xff0c;水能载舟亦能覆舟&#xff0c;一边是不断释放的健身需求&#xff0c;另一边却是无数健身房的闭店潮。 越来越多人倾向于选择家用健身器械来运动或是直接选择无器械的健身运动&#xff0c;比如各类健身操。而…

AngularJS 的生命周期和基础语法

AngularJS 的生命周期和基础语法 文章目录 AngularJS 的生命周期和基础语法1. 使用步骤2. 生命周期钩子函数3. 点击事件4. if 语句1. if 形式2. if else 形式 5. for 语句6. switch 语句7. 双向数据绑定 1. 使用步骤 // 1. 要使用哪个钩子函数&#xff0c;就先引入 import { O…