[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.mfbz.cn/a/582268.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Yolov5简单部署(使用自己的数据集)

一.注意事项 1.本文主要是引用大佬的文章&#xff08;侵权请联系&#xff0c;马上删除&#xff09;&#xff0c;做的工作为简单补充 二.正文 1.大体流程按照 准备&#xff1a;【简单易懂&#xff0c;一看就会】yolov5保姆级环境搭建_哔哩哔哩_bilibili 主要过程&#xff1…

Java | Leetcode Java题解之第55题跳跃游戏

题目&#xff1a; 题解&#xff1a; public class Solution {public boolean canJump(int[] nums) {int n nums.length;int rightmost 0;for (int i 0; i < n; i) {if (i < rightmost) {rightmost Math.max(rightmost, i nums[i]);if (rightmost > n - 1) {retu…

VitePress 构建的博客如何部署到 github 平台?

VitePress 构建的博客如何部署到 github 平台&#xff1f; 1. 新建 github 项目 2. 构建 VitePress 项目 2.1. 设置 config 中的 base 由于我们的项目名称为 vite-press-demo&#xff0c;所以我们把 base 设置为 /vite-press-demo/&#xff0c;需注意前后 / export default…

tidb离线本地安装及mysql迁移到tidb

一、背景&#xff08;tidb8.0社区版&#xff09; 信创背景下不多说好吧&#xff0c;从资料上查tidb和OceanBase“兼容”&#xff08;这个词有意思&#xff09;的比较好。 其实对比了很多数据库&#xff0c;有些是提供云服务的&#xff0c;有些“不像”mysql&#xff0c;综合考虑…

uniapp:K线图,支持H5,APP

使用KLineChart完成K线图制作,完成效果: 1、安装KLineChart npm install klinecharts2、页面中使用 <template><view class="index"><!-- 上方选项卡 --><view class="kline-tabs"><view :style="{color: current==ite…

Windows使用bat远程操作Linux并执行命令

背景&#xff1a;让客户可以简单在Windows中能自己执行 Linux中的脚本&#xff0c;傻瓜式操作&#xff01; 方法&#xff1a;做一个简单的bat脚本&#xff01;能远程连接到Linux&#xff0c;并执行Linux命令&#xff01;客户双击就能使用&#xff01; 1、原先上网查询到使用P…

深度学习:基于Keras框架,使用神经网络模型对葡萄酒类型进行预测分析

前言 系列专栏&#xff1a;机器学习&#xff1a;高级应用与实践【项目实战100】【2024】✨︎ 在本专栏中不仅包含一些适合初学者的最新机器学习项目&#xff0c;每个项目都处理一组不同的问题&#xff0c;包括监督和无监督学习、分类、回归和聚类&#xff0c;而且涉及创建深度学…

《Fundamentals of Power Electronics》——全桥型隔离降压转换器

以下是关于全桥型隔离降压转换器的相关知识点&#xff1a; 全桥变压器隔离型降压转换器如下图所示。 上图展示了一个具有二次侧绕组中心抽头的版本&#xff0c;该电路常用于产生低输出电压。二次侧绕组的上下两个绕组可以看作是两个单独的绕组&#xff0c;因此可以看成是具有变…

CSS-复合选择器

作用&#xff1a; 后代选择器&#xff1a; 子代选择器 并集选择器 用逗号隔开&#xff0c;在style里面写的时候&#xff0c;每一个标签空一行。 <title>Document</title><style>p,div,span{color: aqua;}</style> </head> <body><p>…

Java对象在堆和栈上的存储(对象布局,待完善)

0、前言 这里提到的 Java 对象不仅仅包含引用类型&#xff08;Object&#xff09;&#xff0c;还包含基本数据类型&#xff08;boolean、int、long、float、double&#xff09;。文中部分图片来源于 B站 黑马程序员。 1、在栈上的数据存储 1.1、局部变量 局部变量包含以下情…

C++:map和set的封装

关于红黑树的模拟实现&#xff0c;大家不清楚的先去看看博主的博客再来看这篇文章&#xff0c;因为set和map的封装底层都是利用用的红黑树。所以这里不会过多介绍红黑树的相关内容&#xff0c;而更多的是去为了契合STL中的红黑树去进行改造&#xff0c;让封装的set和map能够去复…

【Java】Java基础 使用集合实现斗地主分牌

&#x1f4dd;个人主页&#xff1a;哈__ 期待您的关注 今天使用集合TreeSet来实现一个斗地主的分牌流程。 TreeSet集合的一个特点就是 元素有序&#xff0c;这样就方便我们分的牌自动排序。 0.思路 1.创建玩家手牌集合 我们到时候分的牌都存储在这里&#xff0c;但你可能会…

华为静音模式指定联系人来电响铃

华为静音模式指定联系人来电响铃 本人7年水果转华为&#xff0c;手机常年静音但是还是想收到指定人来电的。水果这个地方做的是很方便的&#xff0c;直接添加紧急联系人&#xff0c;什么声音都没有&#xff0c;只有指定人的电话铃声 直接上结论&#xff0c;华为是不支持直接这样…

关于google search console工具提交sitemap.xml无法抓取的问题解决办法

其实这个问题很好解决。 第一种情况&#xff1a;利用工具为我们的网站自动生成静态的sitemap.xml文件。这种可以检查下是否完整&#xff0c;然后上传到根目录下去&#xff0c;再去google search console提交我们的网站地图。 第二种情况&#xff1a;同样利用工具自动生成动态s…

【kettle005】kettle访问Oracle数据库并处理数据至execl文件(已更新)

1.一直以来想写下基于kettle的系列文章&#xff0c;作为较火的数据ETL工具&#xff0c;也是日常项目开发中常用的一款工具&#xff0c;最近刚好挤时间梳理、总结下这块儿的知识体系。 2.熟悉、梳理、总结下Oracle数据库相关知识体系 3.欢迎批评指正&#xff0c;跪谢一键三连&am…

MQTT基础知识

mqtt_manul MQTT物联网协议的学习笔记 一、MQTT基础知识 主要优势 发布订阅模式&#xff0c;一对多消息发布基于 TCP/IP 网络连接消息Qos支持&#xff0c;可靠传输保证&#xff08;QoS机制保证可靠传输&#xff09;灵活的消息传输&#xff0c;不关心 Payload 数据格式&…

爬虫的实战应用之短信炸弹playwright现代网页测试工具

不讲废话&#xff0c;先上原理&#xff1a; 短信炸弹&#xff0c;也就是说持续对一个手机进行发送短信&#xff0c;实现的方式就是&#xff0c;利用某些网站的登录 &#xff0c;注册的时候&#xff0c;发送短信验证码来实现。 如下图&#xff0c;其中有一个id为phone的输入框&a…

如何收集EMC Data Domain 的日志

遇到复杂问题&#xff0c;您第一时间需要做的就是收集日志&#xff0c;下面对EMC DataDomain 存储系统&#xff08;有人也叫做VTL虚拟带库&#xff09;如何收集日志做一个详细说明&#xff1a; EMC DD的日志分为两种&#xff0c;一种是Autosupport&#xff0c;一种是support b…

STM32-TIM定时器与PWM输出

学习目标&#xff1a; 1. 熟练掌握 TIM 的参数配置。 2. 掌握通道的参数配置。 3. 深刻理解 PWM 与功率的关系。 4. 理解 PWM 的原理示意。 一 什么是 PWM 输出 PWM &#xff08; pulse width modulation &#xff09;一种脉冲宽度调节技术。 PWM 的效果是什么样子&#xf…

【kettle006】kettle访问华为openGauss高斯数据库并处理数据至execl文件

1.一直以来想写下基于kettle的系列文章&#xff0c;作为较火的数据ETL工具&#xff0c;也是日常项目开发中常用的一款工具&#xff0c;最近刚好挤时间梳理、总结下这块儿的知识体系。 2.熟悉、梳理、总结下华为openGauss高斯数据库相关知识体系 3.欢迎批评指正&#xff0c;跪谢…
最新文章