【C++】拷贝构造函数及析构函数

📢博客主页:https://blog.csdn.net/2301_779549673

📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!

📢本文由 JohnKi 原创,首发于 CSDN🙉

📢未来很长,值得我们全力奔赴更美好的生活✨

文章目录


📢前言

当谈论C++编程语言的核心概念时,拷贝构造函数析构函数无疑是不可或缺的话题。它们不仅仅是理解对象生命周期和内存管理的关键,更是构建复杂系统和高效程序的基础。拷贝构造函数在对象复制过程中扮演着关键角色,决定了如何正确地复制对象的状态和数据。而析构函数则负责在对象生命周期结束时释放资源,确保程序运行的稳定性和性能。

在本博客系列中,我们将深入探讨C++中拷贝构造函数和析构函数的实现原理、使用场景以及最佳实践。我们将从基础知识入手,逐步扩展到高级应用和实际案例分析,帮助读者建立起对这两个重要概念的全面理解和应用能力。无论您是刚入门的初学者,还是希望深化专业知识的资深开发者,本系列都将为您提供有价值的内容和实用的技能,助力您在C++编程的道路上更进一步。让我们一起探索C++世界中的拷贝构造函数和析构函数,发现它们的力量和魅力!


🏳️‍🌈什么是拷贝构造函数

什么是拷贝构造函数

如果已经存在一个对象,我想对这个对象再复制一份,该怎么做呢?

有两种方法拷贝构造赋值运算符重载·,但显然赋值运算符重载不是这里的重点,这里要讲的是前者。至于后者笔者后续再补充。

拷贝构造函数是类的六大特殊成员函数之一,它是构造函数的一个重载形式

而且由于拷贝并不需要改变参数,所以参数部分还要用 "const"来修饰。

比如下面这样一个类

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
	
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

那么它的拷贝构造函数就可以写成这样

cpp 复制代码
	//拷贝构造函数
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

❤️1. 引用传参

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

因为传值传参 是一种拷贝,每次拷贝时又需要先传值传参,就会导致无限递归,导致程序崩溃


顺便提一下传值返回的注意点

传值返回 会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。这种方式避免了额外的拷贝开销,提升了性能。然而,如果返回的对象是函数内部的局部对象,函数结束时该对象的生命周期也随之结束,引用返回就会出现问题,因为引用将指向一个已经销毁的对象,类似于野指针的问题。

为了避免这种情况,需要注意以下几点:

  • 如果函数返回一个对象,且希望通过引用返回以避免拷贝开销,确保返回的对象是通过 new 创建的(即动态分配内存),这样它的生命周期不会受到函数作用域的限制。
  • 对于本地(即函数内部定义的)局部对象,不应该使用引用返回,因为这些对象在函数结束时会被销毁。在这种情况下,应该选择传值返回或者返回一个静态或全局对象,确保返回的对象在函数结束后仍然有效。

因此,在选择传值返回还是传引用返回时,必须根据返回对象的生命周期来进行合理的选择,以确保程序的正确性和性能的最优化。

🧡2. 自动生成的拷贝构造函数

在C++中,如果您没有显式定义拷贝构造函数,编译器将自动生成一个默认的拷贝构造函数。这个自动生成的拷贝构造函数执行的是浅拷贝,即简单地复制对象的每个成员变量的值,即一个一个字节地复制。这种行为对于大多数简单的类和结构体是合适的,可以有效地复制对象的状态。

自动生成的拷贝构造函数在以下情况下会被调用:

对象初始化:使用一个对象初始化另一个对象时,例如 ClassName obj1 = obj2;,编译器会调用自动生成的拷贝构造函数。

对象传递:将对象作为参数传递给函数,或从函数返回对象时,编译器也会使用拷贝构造函数来创建对象的副本。

尽管自动生成的拷贝构造函数对于许多情况都能正常工作,但在某些情况下可能需要显式地定义自定义的拷贝构造函数,特别是当类包含指针动态分配的资源时,以确保正确的深拷贝行为和资源管理。

就比如的创建往往是要动态内存开辟

cpp 复制代码
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	//拷贝函数
	Stack(const Stack& st)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}

	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a ,sizeof(STDataType) * newcapacity);
			if (nullptr == tmp)
			{
				perror("malloc申请失败");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}
	
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

🏳️‍🌈什么是析构函数

析构函数(Destructor 是面向对象编程中的一个概念,它是一种特殊的成员函数,用于在对象生命周期结束时执行清理工作和资源释放操作。在许多编程语言中,包括C++和一些类似的语言,析构函数在对象销毁时自动被调用

在C++中,析构函数的名称与类名相同 ,但前面加上一个波浪号(~)。它没有参数,不返回任何值,也没有返回类型。析构函数的作用是在对象销毁时自动执行一些必要的清理步骤,如释放动态分配的内存、关闭文件、释放资源等。

比如说下面这就是一个基本的析构函数

cpp 复制代码
#include <iostream>

class MyClass {
public:
    // 构造函数
    MyClass() {
        std::cout << "构造函数被调用" << std::endl;
    }

    // 析构函数
    ~MyClass() {
        std::cout << "析构函数被调用" << std::endl;
    }
};

int main() {
    MyClass obj; // 创建对象

    // 在main函数结束时,对象obj将销毁,析构函数会被自动调用
    return 0;
}

但是在C++中,如果您没有显式定义析构函数 ,编译器将自动生成一个默认的析构函数。这个自动生成的析构函数会依次销毁对象的每个成员变量,释放它们占用的内存空间。这对于大多数简单的类和结构体来说通常是合适的,因为它确保对象在生命周期结束时能够正确地释放资源。

自动生成的析构函数在以下情况下会被调用:

  • 对象离开作用域:当对象的作用域结束时,比如一个局部变量超出其作用域范围。
  • 动态分配的对象:如果对象是通过 new 运算符动态分配的,那么在调用 delete 释放内存时,也会自动调用析构函数。

尽管默认的析构函数对于大多数情况都能正常工作,但在涉及到动态分配的资源管理、对象组合等复杂情况下,可能需要显式定义自定义的析构函数。这样可以确保在对象销毁时执行额外的清理工作,如释放动态分配的内存或关闭文件等操作,以防止资源泄漏和程序错误。因此,了解和利用析构函数的自动生成特性对于C++程序的健壮性和性能优化至关重要。

就比如说下面这一块栈的析构函数,因为动态开辟了,所以需要手动释放空间,故要自定义析构函数

cpp 复制代码
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	//拷贝函数
	Stack(const Stack& st)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}

	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a ,sizeof(STDataType) * newcapacity);
			if (nullptr == tmp)
			{
				perror("malloc申请失败");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}

	//析构函数
	~Stack()
	{
		//cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

👥总结

本篇博文对 拷贝构造函数及析构函数 做了一个较为详细的介绍,不知道对你有没有帮助呢

觉得博主写得还不错的三连支持下吧!会继续努力的~

相关推荐
解孔明2 分钟前
IDEA2023.1添加java虚拟机启动参数,打开断言
java·开发语言
关关不烦恼5 分钟前
【Java数据结构】二叉树
java·开发语言·数据结构
苹果酱05676 分钟前
使用 React Testing Library 测试自定义 React Hooks
java·开发语言·spring boot·后端·中间件
好奇的菜鸟15 分钟前
Java技术体系:深入理解JDK与JRE及其产品线
java·开发语言
界面开发小八哥1 小时前
「Qt Widget中文示例指南」如何实现一个系统托盘图标?(二)
开发语言·c++·qt·用户界面
月夕花晨3741 小时前
C++学习笔记(24)
c++·笔记·学习
2301_775602381 小时前
二进制读写文件
开发语言
疑惑的杰瑞1 小时前
[乱码]确保命令行窗口与主流集成开发环境(IDE)统一采用UTF-8编码,以规避乱码问题
java·c++·vscode·python·eclipse·sublime text·visual studio
自身就是太阳1 小时前
深入理解 Spring 事务管理及其配置
java·开发语言·数据库·spring
喵手1 小时前
Java零基础-多态详解
java·开发语言·python