C++ 拷贝构造函数和析构函数

C++ 拷贝构造函数和析构函数

拷贝构造函数

在C++中,拷贝构造函数是一种特殊的构造函数,它用于创建一个新对象作为现有对象的副本。当使用一个已存在的对象来初始化同类型的新对象,或者从函数中返回对象时(虽然大多数现代C++编译器会优化掉这种情况的拷贝),拷贝构造函数就会被调用。拷贝构造函数对于管理动态分配的内存和资源尤其重要,因为它允许开发者控制拷贝过程,确保深拷贝或浅拷贝的正确实施,避免潜在的资源泄露或多次释放。

基本语法

拷贝构造函数的基本形式如下:

ClassName(const ClassName& other);

这里,ClassName代表类名,other是对另一个同类型对象的引用,通常是常量引用。

默认拷贝构造函数

如果没有为类显式定义拷贝构造函数,C++编译器会自动生成一个默认的拷贝构造函数。这个默认的拷贝构造函数执行成员逐个拷贝(Member-wise copy),对于基本类型的成员变量进行直接拷贝,对于类类型的成员则调用其拷贝构造函数进行拷贝。这通常意味着浅拷贝,对于包含动态分配内存或其他需要“深拷贝”处理的资源来说可能不够。

自定义拷贝构造函数

对于需要深拷贝的情况,或者当默认拷贝行为不符合需求时,应该为类定义一个自定义的拷贝构造函数。自定义拷贝构造函数可以确保对象内部状态和资源被正确、完整地复制。

示例

#include <iostream>
#include <cstring>class String {
private:char* data;
public:String(const char* str) { // 构造函数data = new char[strlen(str) + 1];strcpy(data, str);}~String() { // 析构函数delete[] data;}// 自定义拷贝构造函数String(const String& other) {data = new char[strlen(other.data) + 1];strcpy(data, other.data);}void print() {std::cout << data << std::endl;}
};int main() {String str1("Hello");String str2 = str1; // 调用拷贝构造函数str2.print(); // 输出:Helloreturn 0;
}

在这个例子中,String类包含一个指向动态分配内存的指针成员data。自定义的拷贝构造函数确保了当一个String对象被另一个String对象初始化时,进行的是深拷贝,即复制了字符串的内容到新的内存地址,而不仅仅是复制了指针。

注意事项

  • 自定义拷贝构造函数时,要确保正确处理深拷贝,特别是当对象包含指针或其他需要手动管理的资源时。
  • 避免在拷贝构造函数中引发异常,因为这可能导致程序中断,而相关的资源清理可能不会被执行。
  • 当定义了拷贝构造函数时,通常也需要定义赋值操作符重载(Copy Assignment Operator),以保持类行为的一致性。

通过适当地实现拷贝构造函数,可以确保C++程序中的对象复制行为符合预期,避免资源泄漏和其他相关问题,从而提高程序的稳定性和可靠性。

拷贝构造函数调用时机

在C++中,拷贝构造函数是一种特殊的构造函数,用于创建一个新对象作为现有对象的副本。拷贝构造函数的调用时机主要包括以下几种情况:

1. 显式拷贝

当使用一个已存在的对象来显式初始化同类型的新对象时,会调用拷贝构造函数。

ClassName obj1;
ClassName obj2 = obj1; // 显式调用拷贝构造函数

或者直接使用拷贝初始化语法:

ClassName obj2(obj1); // 同样调用拷贝构造函数

2. 函数参数传递

当一个对象作为参数传递给函数,并且参数是按值传递时,会创建该对象的副本,此时会调用拷贝构造函数。

void func(ClassName obj); // 函数声明ClassName obj1;
func(obj1); // 调用拷贝构造函数来传递obj1

3. 函数返回值

当函数返回一个对象,并且返回类型是按值返回时,可能会调用拷贝构造函数来创建返回值的副本。不过,编译器通常会使用返回值优化(Return Value Optimization, RVO)或命名返回值优化(Named Return Value Optimization, NRVO)来避免这种拷贝,但这不是标准要求的行为。

ClassName func() {ClassName obj;return obj; // 可能调用拷贝构造函数,但通常被优化
}

4. 用作异常对象

当抛出异常时,异常对象会被拷贝到异常处理代码中。这个过程会调用拷贝构造函数。

try {throw ClassName(obj); // 抛出obj的副本,调用拷贝构造函数
} catch (ClassName& e) {// 处理异常
}

5. 初始化数组、容器或类成员

当在数组或容器中初始化元素,或者在类中用另一个对象初始化同类型的成员时,也会调用拷贝构造函数。

ClassName array[2] = {obj1, obj1}; // 为数组元素调用拷贝构造函数
std::vector<ClassName> vec(2, obj1); // 为vector元素调用拷贝构造函数class AnotherClass {ClassName member;
public:AnotherClass(ClassName& obj) : member(obj) {} // 为成员变量调用拷贝构造函数
};

总结

拷贝构造函数在对象需要被拷贝创建新实例的场合被调用,这包括显式拷贝、函数参数传递、函数返回值、作为异常对象,以及在初始化数组、容器或类成员时。了解拷贝构造函数的调用时机对于管理对象的生命周期和资源至关重要,尤其是在处理动态分配资源时,正确使用拷贝构造函数可以帮助防止资源泄漏和深浅拷贝问题。

析构函数

在C++中,析构函数是一种特殊的成员函数,它在对象的生命周期结束时自动调用,用于执行对象销毁前的清理工作。析构函数的主要用途是释放对象在生命周期内申请的资源,如动态分配的内存、文件句柄、网络连接等,以避免资源泄露。

基本特性

  • 名称:析构函数的名称由类名前加上波浪符号(~)构成,例如~ClassName()
  • 无参数和返回类型:析构函数不能接受参数,也不返回任何值,甚至void
  • 自动调用:当对象的生命周期结束时(例如,局部对象的作用域结束、动态分配的对象被delete、程序结束时全局或静态对象被销毁),析构函数会被自动调用。
  • 不可重载:每个类只能有一个析构函数,因此析构函数不能被重载。
  • 继承:如果一个类没有显式定义析构函数,编译器会自动生成一个默认的析构函数。但是,默认析构函数只会执行成员对象和基类对象的析构函数,不会处理类作者可能需要手动释放的资源。

示例

class Example {
public:int* data;Example(int size) { // 构造函数data = new int[size]; // 动态分配内存}~Example() { // 析构函数delete[] data; // 释放动态分配的内存}
};

在这个例子中,Example类有一个指向动态分配数组的指针data。在构造函数中分配内存,在析构函数中释放内存。这样,当Example类型的对象生命周期结束时,动态分配的内存会被正确释放,避免内存泄漏。

析构函数的调用时机

  1. 局部对象:当局部对象的作用域结束时(例如,函数执行完毕时)。
  2. 动态分配的对象:通过new关键字动态创建的对象,当使用delete操作时。
  3. 全局或静态对象:程序结束执行时。

注意事项

  • 在管理资源时,应遵循RAII(Resource Acquisition Is Initialization)原则,即在构造函数中获取资源,在析构函数中释放资源,确保资源管理的安全性和简洁性。
  • 对于派生类,如果基类的析构函数不是虚的(virtual),则通过基类指针删除派生类对象可能不会调用派生类的析构函数,导致资源泄露。因此,如果一个类被设计为基类(即预期会有类从它派生),其析构函数应该被声明为虚的。
  • 避免在析构函数中抛出异常。如果析构函数抛出异常,而又没有被捕获,那么程序将直接终止,可能导致其他资源未被正确释放。

通过正确使用析构函数,可以增强C++程序的健壮性和稳定性,避免资源泄露等问题。

析构函数调用时机

在C++中,析构函数是一个特殊的成员函数,它在对象生命周期结束时自动被调用,用于执行对象销毁前的清理工作。析构函数的调用时机主要包括以下几种情况:

1. 局部对象离开作用域

当一个局部对象(在函数内部或任何代码块内部定义的对象)的作用域结束时,该对象的析构函数会被调用。这是因为局部对象在作用域结束时被销毁。

void func() {ClassName obj; // 局部对象// obj的析构函数在这个函数结束时自动调用
}

2. 对象被delete

如果对象是通过new操作符动态分配的,则需要使用delete操作符来释放内存。当delete操作符应用于对象指针时,对象的析构函数会被调用,然后释放分配的内存。

ClassName* obj = new ClassName; // 动态分配
delete obj; // obj的析构函数在这里被调用

3. 对象数组被delete[]

对于通过new[]操作符动态分配的对象数组,使用delete[]操作符来释放内存时,每个数组元素的析构函数都会被依次调用。

ClassName* array = new ClassName[10]; // 动态分配对象数组
delete[] array; // 数组中每个对象的析构函数被调用

4. 通过std::unique_ptrstd::shared_ptr

当使用智能指针(如std::unique_ptrstd::shared_ptr)管理动态分配的对象时,对象的析构函数会在智能指针的生命周期结束时自动调用,例如智能指针离开作用域或显式地重置智能指针。

{std::unique_ptr<ClassName> ptr(new ClassName);
} // ptr离开作用域,管理的对象被销毁,析构函数被调用

5. 非局部静态和全局对象

对于全局对象或静态对象(包括静态局部对象、静态成员变量和命名空间作用域内的静态对象),在程序正常结束执行时(main函数结束或exit函数被调用),这些对象的析构函数会被调用。

ClassName globalObj; // 全局对象int main() {static ClassName staticObj; // 静态局部对象return 0;
} // main函数结束时,globalObj和staticObj的析构函数被调用

总结

析构函数的自动调用机制是C++管理资源和内存的关键部分,确保了即使在面对异常退出或提前返回的情况下,资源也能被正确释放。了解析构函数的调用时机有助于编写更安全、更可靠的C++代码,避免资源泄露和其他资源管理错误。

代码示例

// 自定义字符串类
class String {char* data; // 动态分配的字符数组,用于存储字符串数据int n; // 字符串长度public:// 析构函数:释放动态分配的内存并打印消息~String() {delete[] data;cout << "析构函数" << endl;}// 拷贝构造函数:实现深拷贝String(const String& s) {data = new char[s.n + 1]; // 为data分配足够的内存n = s.n; // 复制字符串长度for (int i = 0; i < n; i++)data[i] = s.data[i]; // 复制字符串数据data[n] = '\0'; // 确保字符串以空字符终止cout << "拷贝构造函数!\n";}// 构造函数:从C风格字符串初始化String(const char* s = 0) {if (s == 0) { // 处理空指针的特殊情况data = 0;n = 0;return;}// 计算输入字符串的长度const char* p = s;while (*p != '\0') p++;n = p - s;// 分配内存并复制字符串data = new char[n + 1];for (int i = 0; i <= n; i++)data[i] = s[i];}// 返回字符串的大小int size() { return n; }// 重载下标运算符[],提供常量访问char operator[](int i) const {if (i < 0 || i >= n) throw "下标非法";return data[i];}// 重载下标运算符[],提供修改访问char& operator[](int i) {if (i < 0 || i >= n) throw "下标非法";return data[i];}
};// 重载<<运算符,实现String类的输出
ostream& operator<<(ostream& o, String s) {for (int i = 0; i < s.size(); i++)cout << s[i];return o;
}int main() {// 测试String类String str, str2("hello world");str2[1] = 'E'; // 使用重载的下标运算符修改字符串cout << str2 << endl; // 使用重载的<<运算符输出字符串String s3 = str2; // 调用拷贝构造函数,进行深拷贝cout << s3 << endl; // 输出拷贝的字符串s3[3] = 'L'; // 修改拷贝的字符串cout << s3 << endl; // 输出修改后的拷贝字符串cout << str2 << endl; // 验证原始字符串未被修改return 0;
}

注意事项

在C++中,拷贝构造函数和析构函数是管理类对象生命周期和资源的关键工具。正确地使用它们对于防止资源泄露、避免未定义行为和提升代码效率至关重要。以下是使用拷贝构造函数和析构函数时的一些重要注意事项。

拷贝构造函数的注意事项

  1. 深拷贝 vs. 浅拷贝

    • 当类成员包含指向动态分配内存的指针时,应实现深拷贝以独立复制数据到新对象,防止原始对象和拷贝对象指向同一内存。
    • 默认的拷贝构造函数只进行浅拷贝,即直接复制成员的值(包括指针的地址),可能导致双重释放或悬挂指针问题。
  2. 自我赋值安全

    • 在拷贝构造函数(以及赋值运算符)的实现中,检查自我赋值的情况是一个好习惯,尽管在拷贝构造函数中自我赋值的情况较少见。
  3. 异常安全

    • 拷贝构造函数在执行过程中可能因为资源分配失败(如new操作)而抛出异常。实现拷贝构造函数时应考虑其异常安全性,确保程序的健壮性。

析构函数的注意事项

  1. 资源释放

    • 析构函数应释放对象在生命周期内申请的所有资源,如动态分配的内存、打开的文件句柄、网络连接等,避免资源泄露。
  2. 不要抛出异常

    • 析构函数不应抛出异常。如果析构函数可能会抛出异常,应在析构函数内部捕获这些异常,防止异常传播出去。因为在对象销毁过程中抛出的异常很难被正确处理,并且可能导致程序终止。
  3. 虚析构函数

    • 如果一个类被设计为基类(即预期会有其他类继承自该类),应将析构函数声明为虚函数。这确保了通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,从而正确地释放资源。
  4. 析构顺序

    • 对象的析构顺序与构造顺序相反。对于派生类对象,首先调用派生类的析构函数,然后是基类的析构函数。对于类成员对象,它们按照声明顺序的逆序被析构。

结合示例中的String类

  • 拷贝构造函数:实现了深拷贝,为data成员分配新的内存,并逐字符复制内容。这是必要的,因为类管理了动态分配的内存。

  • 析构函数:释放了data成员指向的内存,并打印了消息。这确保了动态分配的内存被正确释放。

  • 输出运算符重载:虽然不是拷贝构造函数或析构函数,但值得注意的是,它应该接受const String&而不是String来避免不必要的拷贝,同时提高效率和减少动态内存分配。

正确管理资源并遵循上述注意事项,有助于编写出安全、高效且易于维护的C++代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/758921.shtml

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

相关文章

Matlab|基于多目标粒子群算法的配电网储能选址定容

目录 一、主要内容 二、主要流程 三、部分程序 四、程序结果 五、程序链接 一、主要内容 程序是对文章《基于多目标粒子群算法的配电网储能选址定容》的方法复现&#xff0c;具体内容如下&#xff1a; 以系统节点电压水平&#xff08;电网脆弱性&#xff09;、网络损耗以及…

使用poi向excel中插入附件(以pdf为例)

最近在使用easyExcel操作excel文件时&#xff0c;一直想找到一个方法可以往excel中填充附件&#xff0c;但是目前只发现poi可以插入附件&#xff0c;于是将方法记录如下&#xff1a; public class poiTest {/*** 写个main方法来做一个测试* param args*/public static void ma…

数据库系统概论-第5章 数据库完整性

5.1 实体完整性 5.2 参照完整性 5.3 用户定义完整性 5.4 完整性约束命名子句 5.5 域中的完整性限制 5.6 断言 5.7 触发器 5.8 小结

Pytest自动化测试框架快速上手(超详细)

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 关注公众号&#xff1a;互联网杂货铺&#xff0c;回复1 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;薪资嘎嘎涨 pytest是一个非常成熟的全功能的Python测试框架&#…

蓝桥杯单片机快速开发笔记——NE555测频

一、原理分析 NE555作为一种多功能集成电路&#xff0c;在信号发生和频率测量方面具有广泛的应用。通过合理配置和连接外部元件&#xff0c;可以实现不同类型的信号发生和频率测量功能。 原理&#xff1a; 信号发生器&#xff1a; NE555可以配置为多种不同的振荡器电路&#x…

【鸿蒙HarmonyOS开发笔记】通知模块之发布基础类型通知,内含如何将图片变成PixelMap对象

通知简介 应用可以通过通知接口发送通知消息&#xff0c;终端用户可以通过通知栏查看通知内容&#xff0c;也可以点击通知来打开应用。 通知常见的使用场景&#xff1a; 显示接收到的短消息、即时消息等。 显示应用的推送消息&#xff0c;如广告、版本更新等。 显示当前正…

前后端路径一致报Request failed with status code 404

场景&#xff1a; 前后端路径一致报Request failed with status code 404 解决方案&#xff1a; 1&#xff1a;检查网关是否增加路径和重启网关模块 2&#xff1a;检查是否controller加注解 3&#xff1a;检查前后端路径是否一致注意路径是否带空格

基于cnn深度学习的yolov5+pyqt+分类+resnet+骨龄检测系统

往期热门博客项目回顾&#xff1a; 计算机视觉项目大集合 改进的yolo目标检测-测距测速 路径规划算法 图像去雨去雾目标检测测距项目 交通标志识别项目 yolo系列-重磅yolov9界面-最新的yolo 姿态识别-3d姿态识别 深度学习小白学习路线 YOLOv5与骨龄识别 YOLOv5&a…

极简生活|2024年让自己越来越好的18个极简好习惯

哈喽&#xff0c;你好啊&#xff0c;我是雷工&#xff01; 转眼间已经进入了2024年&#xff0c;新的一年&#xff0c;新的开始。 俗话说&#xff1a;百尺高台起于垒土&#xff0c;千里之堤毁于蚁穴。 好习惯积累的越多&#xff0c;坏习惯越来越少&#xff0c;我们的生活才能越…

春招面试高频题目总结

面试问题 redis 可以用于进程间通信吗&#xff1f; Why&#xff1f;How? ---> 延展一下 有哪些进程间通信技术, 优劣如何&#xff1f; 有大量的插入sql语句&#xff0c;一条条的插入性能很差&#xff0c;如何通过事务进行优化&#xff1f; 保证线程安全的策略有哪些&…

【NLP笔记】预训练+微调范式之OpenAI Transformer、ELMo、ULM-FiT、Bert..

文章目录 OpenAI TransformerELMoULM-FiTBert基础结构Embedding预训练&微调 【原文链接】&#xff1a; BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding 【本文参考链接】 The Illustrated BERT, ELMo, and co. (How NLP Cracked Tra…

STL第一弹

2 STL初识 2.1 STL的诞生 长久以来&#xff0c;软件界一直希望建立一种可重复利用的东西 C的面向对象和泛型编程思想&#xff0c;目的就是复用性的提升 大多情况下&#xff0c;数据结构和算法都未能有一套标准,导致被迫从事大量重复工作为了建立数据结构和算法的一套标准,诞生…

JAVA_Response

1.完成重定向 重定向代码实现 1.重定向资源跳转的方法 //1.设置状态码为302 response.setStatus(302); //2.设置响应头location response.setHeader("location","/xxx/aaa"); 2.简单的方法 response.sendRedirect("/xxx/aaa");重定向特点数据(…

Gradio官方文档

文章目录 构建您的第一个demo分享您的demo进度条受密码保护的应用程序The Interface class&#xff08;接口类&#xff09;Components Attributes&#xff08;组件属性&#xff09;多个输入和输出组件图像示例嵌套列表描述性内容手风琴中的附加输入The 4 Kinds of Gradio Inter…

蓝桥杯Python B组练习——完美的代价

一、题目 问题描述   回文串&#xff0c;是一种特殊的字符串&#xff0c;它从左往右读和从右往左读是一样的。小龙龙认为回文串才是完美的。现在给你一个串&#xff0c;它不一定是回文的&#xff0c;请你计算最少的交换次数使得该串变成一个完美的回文串。   交换的定义是…

C语言-----冒泡排序

今天&#xff0c;让我们来学习一下C语言中一个简单的排序算法------冒泡排序。 什么是冒泡排序呢&#xff1f; 冒泡排序是C语言中一个可以将一个数组的内容按照升序或者降序进行重新排列的算法。简单来说&#xff0c;是一种排序的思维。 冒泡排序的核心思想&#xff1a;让同…

【手撸IM】通讯协议设计与实现

【手撸IM】专题由来&#xff1a;之前利用业余空闲时间写一个Java版Akka-Rpc&#xff0c;并且基于它写了一个分布式高性能文件服务&#xff0c;从反馈上来看&#xff0c;还是得到了一定的关注&#xff0c;甚至部分同学真的基于此直接抄作业和二开了。因此有了再进一步去手撸一个…

【Vue】el-select下选组件

系列文章 【Vue】vue增加导航标签 本文链接&#xff1a;https://blog.csdn.net/youcheng_ge/article/details/134965353 【Vue】Element开发笔记 本文链接&#xff1a;https://blog.csdn.net/youcheng_ge/article/details/133947977 【Vue】vue&#xff0c;在Windows IIS平台…

【ROS | OpenCV】在ROS中实现多版本OpenCV、cv_bridge共存:安装与配置指南

在 Ubuntu 20.04 中&#xff0c;ROS Noetic 默认安装的 OpenCV 版本为 4.2.0。如果您需要确认系统中已安装的 OpenCV 版本&#xff0c;可以使用以下命令&#xff1a; sudo find / -iname "*opencv*"然而&#xff0c;许多开源算法都是基于 OpenCV 3 编写的&#xff0…

修改约束

目录 修改约束 创建数据库 添加约束 删除约束 Oracle从入门到总裁:​​​​​​https://blog.csdn.net/weixin_67859959/article/details/135209645 修改约束 如果说表结构的修改还在可以容忍的范畴之内&#xff0c;那么约束的修改是绝对 100% 禁止的 所有的约束一定要在…