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

news2024/11/25 20:54:00


一、构造函数

构造函数是特殊的成员函数,它在创建对象时自动调用。其主要作用是初始化对象的成员变量(不是开辟空间)。构造函数的名字必须与类名相同,且没有返回类型(即使是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.coloradmin.cn/o/1632438.html

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

相关文章

222.完全二叉树的节点个数

题目链接 题目描述 给你一棵 完全二叉树 的根节点 root &#xff0c;求出该树的节点个数。 完全二叉树 的定义如下&#xff1a;在完全二叉树中&#xff0c;除了最底层节点可能没填满外&#xff0c;其余每层节点数都达到最大值&#xff0c;并且最下面一层的节点都集中在该层最…

vue2—— mixin 超级详细!!!

mixin Mixin是面向对象程序设计语言中的类&#xff0c;提供了方法的实现。其他类可以访问mixin类的方法而不必成为其子类 Mixin类通常作为功能模块使用&#xff0c;在需要该功能时“混入”&#xff0c;有利于代码复用又避免了多继承的复杂 vue中的mixin 先来看一下官方定义 mi…

【麒麟(Linux)系统远程连接到windows系统并进行文件传输】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言使用步骤总结 前言 一般来说&#xff0c;windows自带远程桌面&#xff0c;使用的RDP协议&#xff0c;Linux上支持RDP协议的软件很多&#xff0c;常用的是Remmi…

基于决策树的DDoS攻击检测与防护系统研究---实验/论文

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

【前端热门框架【vue框架】】——条件渲染和列表渲染的学习的秒杀方式

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;程序员-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;v…

HTTPS证书申请:相关流程及注意事项

申请HTTPS证书的过程主要包括以下几个步骤&#xff0c;以及一些需要注意的事项&#xff1a; 申请流程&#xff1a; 1. 选择证书类型和期限&#xff1a; - 根据需求选择合适的SSL证书类型&#xff0c;常见的有DV&#xff08;域名验证&#xff09;、OV&#xff08;组织验证&#…

Unity 问题之 开发应用在设备上运行闪屏花屏问题的分析处理

Unity 问题之 开发应用在设备上运行闪屏花屏问题的分析处理 目录 Unity 问题之 开发应用在设备上运行闪屏花屏问题的分析处理 一、简单介绍 二、问题现象 三、问题分析 四、使用空后处理&#xff0c;解决闪屏花屏的显示问题 五、空后处理完整代码 一、简单介绍 Unity 在…

鸿蒙开发HarmonyOS4.0入门与实践

鸿蒙开发HarmonyOS4.0 配合视频一起食用&#xff0c;效果更佳 课程地址&#xff1a;https://www.bilibili.com/video/BV1Sa4y1Z7B1/ 源码地址&#xff1a;https://gitee.com/szxio/harmonyOS4 准备工作 官网地址 鸿蒙开发者官网&#xff1a;https://developer.huawei.com/con…

使用FPGA发送一个经过曼彻斯特编码的伪随机序列

介绍 这几天突然就不知道要使用FPGA实现什么样的功能了,然后就跑去学习数电了,学的也是晕晕的。正好之前写了一个使用FPGA发送伪随机序列的代码,然后因为需要使用曼彻斯特编码,所以又加了一个模块吧,使得最后输出的波形经过曼彻斯特编码。 曼彻斯特编码 首先,曼彻斯特编…

【操作系统复习资料】(持续更新中)

目录 第一章&#xff1a;操作系统引论 第二章&#xff1a;进程的描述与控制 未完待续。。。。。接 第三章&#xff1a;处理机调度与死锁 第四章&#xff1a;存储器管理 第五章&#xff1a;虚拟存储器 第六章&#xff1a;第八节 磁盘存储器的性能和调度 第一章&#xff1a…

pgvector扩展在IvorySQL Oracle兼容模式下的应用实践

向量数据库是生成式人工智能(GenAI)的关键组成部分。作为PostgreSQL的重要扩展&#xff0c;pgvector支持高达16000维的向量计算能力&#xff0c;使得PostgreSQL能够直接转化为高效的向量数据库。 IvorySQL基于PostgreSQL开发&#xff0c;因此它同样支持添加pgvector扩展。在Ora…

社交媒体数据恢复:新浪微博

当我们在使用新浪微博时&#xff0c;可能会遇到一些意外情况&#xff0c;如误删微博、账号出现问题等。这时&#xff0c;我们需要进行数据恢复。本文将详细介绍如何在新浪微博中进行数据恢复。 首先&#xff0c;我们需要了解新浪微博的数据恢复功能。根据微博的帮助中心&#…

实验8 顺序图、状态图

一、实验目的 通过绘制顺序图、状态图&#xff0c;掌握顺序图、状态图之间的基本原理和差异。 能对简单问题进行顺序图、状态图的分析与绘制。 二、实验项目内容&#xff08;实验题目&#xff09; 在图书信息管理系统中&#xff0c;系统管理员可以对图书信息进行管理和维护…

Python轻量级Web框架Flask(12)—— Flask类视图实现前后端分离

0、前言&#xff1a; 在学习类视图之前要了解前后端分离的概念&#xff0c;相对于之前的模板&#xff0c;前后端分离的模板会去除views文件&#xff0c;添加两个新python文件apis和urls&#xff0c;其中apis是用于传输数据和解析数据 的&#xff0c;urls是用于写模板路径的。 …

merge and rebase

文章目录 什么是merge什么是rebasemerge和rebase的区别操作执行git merge操作git rebase操作冲突解决解决冲突的步骤 Git Merge 和 Git Rebase 都是用于集成来自不同分支的修改的 Git 命令。 什么是merge Git Merge 是将一个分支的改动合并到另一个分支的方式。当你执行一个 m…

Unity 物体触碰事件监听

声明委托 public delegate void MyDelegate(Collider trigger); C# 委托&#xff08;Delegate&#xff09; | 菜鸟教程 (runoob.com)https://www.runoob.com/csharp/csharp-delegate.html 定义委托 public MyDelegate onTriggerEnter; public MyDelegate onTriggerStay; pu…

Leetcode—1041. 困于环中的机器人【中等】

2024每日刷题&#xff08;121&#xff09; Leetcode—1041. 困于环中的机器人 实现代码 class Solution { public:bool isRobotBounded(string instructions) {int x 0;int y 0;int d 0;vector<vector<int>> direction{{0, 1}, {1, 0}, {0, -1}, {-1, 0}};for…

C语言阶段的题目解析

前言 我们C语言已经学习的差不多了&#xff0c;但是C语言之中存在的一些问题与难点我们还不一定能够又快又好地解决&#xff0c;为了夯实我们的基础&#xff0c;我们来练习几道稍微有点难度的C语言习题吧 例题一 题目 int main(void) {unsigned char i 7;int j 0;for (; i…

织梦云端:网络信号原理的艺术解码

hello &#xff01;大家好呀&#xff01; 欢迎大家来到我的Linux高性能服务器编程系列之《织梦云端&#xff1a;网络信号原理的艺术解码》&#xff0c;在这篇文章中&#xff0c;你将会学习到网络信号原理以及应用&#xff0c;并且我会给出源码进行剖析&#xff0c;以及手绘UML图…

MQTT数据传输Payload的常见格式介绍

使用MQTT client过程中看到常见的数据格式&#xff1a; 下面是介绍 Plaintext&#xff08;纯文本&#xff09; 介绍&#xff1a;纯文本编码是最基本的编码形式&#xff0c;它使用标准的ASCII或Unicode字符来表示数据。这种编码格式是人类可读的&#xff0c;因为它直接表示文本信…