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

JhonKI 2024-07-24 08:35:08 阅读 85

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

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

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

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

在这里插入图片描述

在这里插入图片描述

文章目录

📢前言🏳️‍🌈什么是拷贝构造函数❤️1. 引用传参🧡2. 自动生成的拷贝构造函数

🏳️‍🌈什么是析构函数👥总结


📢前言

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

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


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

什么是拷贝构造函数

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

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

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

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

比如下面这样一个类

#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;

};

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

//拷贝构造函数

Date(const Date& d)

{

_year = d._year;

_month = d._month;

_day = d._day;

}

❤️1. 引用传参

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

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

在这里插入图片描述

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

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

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

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

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

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

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

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

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

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

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

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

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

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

#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++程序的健壮性和性能优化至关重要。

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

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;

};


👥总结

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

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

请添加图片描述



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。