[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,一经查实,立即删除!

相关文章

Vue2动态添加属性方式

Vue2不允许在已经创建的实例上动态添加新的响应式属性;若想实现数据与视图同步更新&#xff0c;可采取下面两种解决方案&#xff1a; 1.使用Vue.set&#xff0c;里面的参数是Vue.set(target,index, value) // 部分代码 export default {data() {return {info: {username: ala…

【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;效果一览基本介绍程序设计参考资料 效果一览 基…

【八股】UML 2的基础结构和上层结构

UML&#xff08;统一建模语言&#xff09;是一种广泛使用的建模语言&#xff0c;用于软件工程中的系统设计。UML 2是UML的一个重要版本&#xff0c;它扩展了原有的功能&#xff0c;提供了更加丰富和灵活的建模能力。UML 2的结构可以分为两部分&#xff1a;基础结构&#xff08;…

C++ //练习 14.8 你在7.5.1节的练习7.40(第261页)中曾经选择并编写了一个类,为它定义一个输出运算符。

C Primer&#xff08;第5版&#xff09; 练习 14.8 练习 14.8 你在7.5.1节的练习7.40&#xff08;第261页&#xff09;中曾经选择并编写了一个类&#xff0c;为它定义一个输出运算符。 环境&#xff1a;Linux Ubuntu&#xff08;云服务器&#xff09; 工具&#xff1a;vim 代…

WPF中Nlog的使用--能够提取Nlog的日志内容,并定义使用

背景 使用Nlog日志进行本地文件输出,以及进行定时清理删除参考其他文章 在用户本地计算机上进行Nlog输出,或者使用Web的Http的Post请求输出到后端数据库使用File或者Web的数据类型就可以了,但是我这里希望把我的日志进行输出到自己的云端,但是存在如下情况: 1、用户在一天…

孩子多大可以学编程,需要具备哪些基础知识?

孩子学习编程的最佳年龄取决于孩子的兴趣和学习能力。一般来说&#xff0c;孩子从5岁开始就可以接触编程教育&#xff0c;但更多的孩子在7到12岁之间开始学习编程。这个年龄段的孩子通常具有较好的逻辑思维能力和抽象思维能力&#xff0c;更容易理解编程的概念。 要学习编程&a…

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

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

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

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

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

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

【ARM 常见汇编指令学习 6.1 - armv8 乘加指令 madd详细介绍】

请阅读【嵌入式开发学习必备专栏 】 文章目录 armv8 乘加指令 madd使用场景示例注意事项 armv8 乘加指令 madd 在ARMv8架构中&#xff0c;madd指令是一种乘加指令&#xff0c;用于执行两个数的乘法操作&#xff0c;并将结果与第三个数相加。madd指令是“Multiply-Add”的缩写&…

CentOS系统如何设置系统默认语言以及命令行输出的语言为英文? locale

要点 locale是地区相关&#xff1a;The locale command displays information about the current locale, or all locales, on standard output.通过全局变量进行配置&#xff0c;后台应该有程序读取环境变脸后进行输出确认。全局变量包括LANG和LC_ALL系统启动后首先初次读取/…

【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;协程的优点协程代码实例 进程、线程和协程适合的任务性质和环境多进程更适合的场景多线程更适合的场…

智能优化算法--计算重复运行50次的最大值、最小值、均值、标准差

⚠申明&#xff1a; 未经许可&#xff0c;禁止以任何形式转载&#xff0c;若要引用&#xff0c;请标注链接地址。 全文共计3077字&#xff0c;阅读大概需要3分钟 &#x1f308;更多学习内容&#xff0c; 欢迎&#x1f44f;关注&#x1f440;【文末】我的个人微信公众号&#xf…

【ARM Cache 系列文章 11 -- ARM Cache 直接映射 详细介绍】

请阅读【ARM Cache 系列文章专栏导读】 文章目录 ARM Cache组织形式直接映射(Direct Mapped)直接映射示例直接映射原理Cache颠簸(cache thrashing)原因文章:【ARM Cache 系列文章 11.1 – ARM Cache 全相连 详细介绍】 文章:【ARM Cache 系列文章 11.2 – ARM Cache 组相…

在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…