STL与拷贝构造函数

本文探讨了STL容器在存放包含动态分配内存的对象时如何处理深拷贝问题,通过具体示例展示了拷贝构造函数及析构函数的调用过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

所有容器提供的都是“value语意”而非“reference语意”。容器内进行元素的安插操作时,内部实施的是拷贝操作,置于容器内。因此STL容器的每一个元素都必须能够拷贝。---<<C++标准程序库>> 侯捷、孟岩译 p144页原文 

以vector为例,往Vector中(实际上所有STL容器都是这样)放元素,Vector会调用元素类的拷贝构造函数生成的副本,当Vector走出生存期时(),会自动调用其中每个元素的析构函数。比如,如果 vector<myclass> a,然后a.push_back(b);push_back其实是调用myclass的拷贝构造函数将参数b拷贝进去的。由于vector是自动管理的,出了a的作用域外,a会自动消失释放内存。之前push_back到a里面的数据b通过调用b.~myclass()释放内存。

[cpp]  view plain  copy
 print ?
  1. #include <vector>   
  2. using namespace std;   
  3.   
  4. class CDemo{   
  5. public:   
  6. CDemo():str(NULL){}   
  7. ~CDemo(){if(str) delete [] str;}   
  8. char *str;   
  9. };   
  10.   
  11. int main()   
  12. {   
  13. CDemo d1;   
  14. d1.str = new char[32];   
  15. strcpy(d1.str, "trend micro");   
  16.   
  17. vector <CDemo> *a1 = new vector <CDemo>();   
  18. a1 -> push_back(d1);   
  19.   
  20. delete a1;   
  21. return 0;   
  22. }   

1. vector <CDemo> *a1 = new vector <CDemo>(); a1是new出来的,所以必须要手工delete.这是对a1本身而言,而与a1内存储的数据无关。
2. a1 -> push_back(d1); 这部操作比较复杂,因为你的vector是存储类,而不是类指针。但是push_back的参数是引用,所以不需要创建d1的拷贝(也就不需要调用拷贝构造)作为参数传递给push_back。但是在push_back中,会创建d1的拷贝d1_1(需要调用拷贝构造),d1_1是存储在a1管理的内存中。
3. delete a1; a1中存有d1_1,所以会删除d1_1,自然会调用d1_1的析构函数。 
4. 在main中return 0, d1被自动删除,此时调用d1的析构函数。 
5. 因为class CDemo没有拷贝构造函数,所以创建拷贝时只是简单的把新对象中每个成员变量的值设置成与原来的对象相等。相当于运行memcpy。这时问题就来了,因为你的一个成员是char *str; 这样d1,d1_1的str都是指向同一个地址。所以只有第一次调用CDemo的析构函数时能运行正确,以后的都会出错。因为一个地址只能释放一次。

解决办法是定义自己的拷贝构造函数实现深拷贝:

[cpp]  view plain  copy
 print ?
  1. #include <iostream>  
  2. #include <fstream>  
  3. #include <vector>  
  4. using namespace std;  
  5.   
  6. ofstream out("test.out");  
  7. class CDemo{  
  8. public:  
  9.     CDemo():str(NULL){  
  10.         out << "constructor is called" << endl;  
  11.     }  
  12.     CDemo(const CDemo &cd)  
  13.     {  
  14.         out << "copy constructor is called" << endl;  
  15.         this->str = new char[strlen(cd.str)+1];  
  16.         strcpy(str, cd.str);  
  17.     }  
  18.   
  19.     ~CDemo(){  
  20.         if(str){  
  21.             out << "destructor is called" << endl;  
  22.             delete[] str;  
  23.         }  
  24.     }  
  25.     char *str;  
  26. };  
  27.   
  28. int main()  
  29. {  
  30.     vector <CDemo> *a1 = new vector <CDemo>();  
  31.     a1 -> reserve(1);  
  32.     out << a1 -> capacity() << endl;  
  33.       
  34.     CDemo d1;  
  35.     d1.str = new char[32];  
  36.     strcpy(d1.str, "trend micro1");  
  37.     out << "/////////////////////////////////" << endl;  
  38.     a1->push_back(d1);  
  39.     out << "/////////////////////////////////" << endl;  
  40.     CDemo d2;  
  41.     d2.str = new char[32];  
  42.     strcpy(d2.str, "trend micro2");  
  43.     out << "/////////////////////////////////" << endl;  
  44.     a1->push_back(d2);  
  45.     out << "/////////////////////////////////" << endl;  
  46.     delete a1;  
  47.   
  48.     return 0;  
  49. }  
1
constructor is called
/////////////////////////////////
copy constructor is called
/////////////////////////////////
constructor is called
/////////////////////////////////
copy constructor is called
copy constructor is called         //搬运之前的元素
destructor is called                    //之前的元素被析构
/////////////////////////////////
destructor is called                    //析构容器中所有对象
destructor is called
destructor is called                    //析构CDemo d1
destructor is called                   //析构CDemo d2

可以看到,再加入第二个对象的时候,拷贝构造函数被调用的两次,这是因为,在第一次push_back时,vector的capacity是1,是正常调用一次拷贝构造;而第二次push_back时,会发现容量不够了,stl会重新分配一个old_size的两倍的空间,除了新push进来的数据会放在这个新空间,调用一次拷贝构造,原来已经有的数据也要搬过来的,就又要调用拷贝构造。如果把a1 -> reserve(1);修改为a1 -> reserve(2);保证第二次时也有足够的空间,那么程序的运行结果为:

2
constructor is called
/////////////////////////////////
copy constructor is called
/////////////////////////////////
constructor is called
/////////////////////////////////
copy constructor is called
/////////////////////////////////
destructor is called
destructor is called
destructor is called
destructor is called
为了进一步验证空间重新分配对对象拷贝的影响,看下面的例子:

[cpp]  view plain  copy
 print ?
  1. #include <iostream>  
  2. #include <fstream>  
  3. #include <vector>  
  4. using namespace std;  
  5.   
  6. ofstream out("test.out");  
  7. class CDemo{  
  8. public:  
  9.     CDemo():str(NULL){  
  10.         out << "constructor is called" << endl;  
  11.     }  
  12.     CDemo(const CDemo &cd)  
  13.     {  
  14.         out << "copy constructor is called" << endl;  
  15.         this->str = new char[strlen(cd.str)+1];  
  16.         strcpy(str, cd.str);  
  17.     }  
  18.   
  19.     ~CDemo(){  
  20.         if(str){  
  21.             out << "destructor is called" << endl;  
  22.             delete[] str;  
  23.         }  
  24.     }  
  25.     char *str;  
  26. };  
  27.   
  28. int main()  
  29. {  
  30.     vector <CDemo> *a1 = new vector <CDemo>();  
  31.     a1 -> reserve(1);  
  32.       
  33.     for(int i = 1; i < 5; i ++){  
  34.         out << "/////////////////////////////////" << endl;  
  35.         out << i << endl;  
  36.         CDemo d;  
  37.         d.str = new char[32];  
  38.         strcpy(d.str, "trend micro1" + i);  
  39.         out << "begin to push_back" << endl;  
  40.         out << "the vector capacity is " << a1 -> capacity() << endl;  
  41.         a1->push_back(d);  
  42.     }  
  43.     out << "/////////////////////////////////" << endl;  
  44.     delete a1;  
  45.   
  46.     return 0;  
  47. }  
程序的运行结果:

/////////////////////////////////
1
constructor is called
begin to push_back
the vector capacity is 1
copy constructor is called
destructor is called
/////////////////////////////////
2
constructor is called
begin to push_back
the vector capacity is 1
copy constructor is called
copy constructor is called          //搬运之前的元素1
destructor is called                     //之前的元素被析构
destructor is called                     //CDemo d临时对象
/////////////////////////////////
3
constructor is called
begin to push_back
the vector capacity is 2
copy constructor is called
copy constructor is called         //搬运之前的元素1
copy constructor is called         //搬运之前的元素2
destructor is called                    //之前的元素1被析构
destructor is called                    //之前的元素2被析构
destructor is called                    //CDemo d临时对象
/////////////////////////////////
4
constructor is called
begin to push_back
the vector capacity is 4
copy constructor is called         //不需要搬运,第三次时容量已经变成4
destructor is called                    //CDemo d临时对象
/////////////////////////////////
destructor is called                    //析构容器中所有对象
destructor is called
destructor is called
destructor is called
可以看到,容量的确是按照两倍的空间递增,并且原来已经有的数据要搬过来,就要调用拷贝构造。所以为了程序的效率,最好一开始就用reserve确定vector的大小,避免之后动态扩展,搬运原有数据引起拷贝构造的调用。

<think>好的,我现在需要详细解释拷贝构造函数,并列举其使用场景。首先,我要确保自己对拷贝构造函数有全面的理解,然后整理出用户可能需要的关键点。 拷贝构造函数是C++中的一个特殊成员函数,用于创建一个新对象作为现有对象的副本。它的基本形式是`ClassName(const ClassName& other)`。当对象被以值传递、返回或以值方式初始化时,拷贝构造函数会被调用。理解这一点很重要,因为它关系到对象的复制方式和资源管理。 接下来,我需要考虑用户可能遇到的问题。比如,为什么需要自定义拷贝构造函数?默认的拷贝构造函数有什么问题?这时候深拷贝和浅拷贝的区别就变得关键。特别是当类中有动态分配的资源时,默认的浅拷贝会导致多个对象指向同一内存,引发双重释放等问题。因此,必须通过自定义拷贝构造函数实现深拷贝,分配新内存并复制内容。 然后,使用场景部分需要覆盖常见的几种情况:显式拷贝构造、函数参数传递、函数返回值以及容器中的对象复制。每个场景都需要具体例子来说明拷贝构造函数何时被调用,比如在传递对象参数时,值传递会触发拷贝构造,而引用传递则不会。同时,编译器优化如返回值优化(RVO)可能会影响拷贝构造函数的调用次数,这也是需要注意的地方。 用户可能还会想知道如何正确实现拷贝构造函数,避免常见错误。比如,确保深拷贝的正确性,处理可能的异常,以及遵循三大法则(拷贝构造函数拷贝赋值运算符、析构函数的一致性)。此外,现代C++中的移动语义和`=delete`的使用也是值得提及的改进点,帮助用户了解如何禁用拷贝或转移资源所有权。 另外,我需要验证默认拷贝构造函数的风险,通过对比有无自定义拷贝构造函数时的程序行为,展示内存地址的变化和潜在的双重释放错误。这部分可以通过示例代码的输出结果直观说明问题,增强用户的理解。 最后,总结最佳实践,比如何时需要自定义拷贝构造函数,如何结合其他特殊成员函数管理资源,以及现代C++提供的工具和技巧。确保用户不仅了解拷贝构造函数的机制,还能在实际编程中正确应用,避免常见陷阱。 整理这些思路时,要注意结构清晰,逐步引导用户理解概念,并通过代码示例和输出来验证关键点。同时,语言要简洁明了,避免过于技术化的术语,确保不同水平的读者都能跟上。</think>### 拷贝构造函数详解使用场景 --- #### **一、基本定义** **拷贝构造函数**(Copy Constructor)是C++中用于**创建新对象作为现有对象副本**的特殊构造函数,其核心特征如下: - **语法形式**:`ClassName(const ClassName& other)` - **调用时机**:当对象通过**值传递**、**值返回**或**显式复制**时触发 - **默认行为**:若未显式定义,编译器会生成**浅拷贝**版本(逐成员复制) --- #### **二、核心作用:深拷贝资源管理** 当类包含**动态分配的资源**(如指针、文件句柄)时,必须自定义拷贝构造函数实现**深拷贝**,避免以下问题: ```cpp // 示例:未定义拷贝构造的灾难性后果 class BadString { char* data; public: BadString(const char* str) { data = new char[strlen(str)+1]; strcpy(data, str); } ~BadString() { delete[] data; } // 析构函数 }; int main() { BadString a("Hello"); BadString b = a; // 默认浅拷贝:b.data a.data 指向同一内存 return 0; // 程序崩溃:双重释放! } ``` --- #### **三、典型使用场景** | 场景类型 | 触发条件示例 | 代码示例 | |-------------------------|----------------------------------|-----------------------------------| | **显式对象复制** | 通过现有对象初始化新对象 | `MyClass obj2(obj1);` | | **函数参数值传递** | 对象作为非引用参数传递 | `void func(MyClass obj) { ... }` | | **函数返回值** | 返回局部对象(可能触发RVO优化) | `return MyClass(...);` | | **容器操作** | STL容器插入/复制元素 | `vector.push_back(existing_obj);` | | **异常抛出** | 抛出异常对象时复制 | `throw MyException(obj);` | --- #### **四、完整实现示例** ```cpp #include <iostream> #include <cstring> class SecureArray { private: int* m_data; size_t m_size; public: // 普通构造函数 SecureArray(size_t size) : m_size(size) { m_data = new int[size]{0}; // 初始化为0 } // 拷贝构造函数(深拷贝) SecureArray(const SecureArray& other) : m_size(other.m_size) { std::cout << "执行深拷贝,分配 " << m_size*4 << " 字节内存" << std::endl; m_data = new int[m_size]; memcpy(m_data, other.m_data, m_size * sizeof(int)); } // 析构函数 ~SecureArray() { delete[] m_data; std::cout << "释放 " << m_size*4 << " 字节内存" << std::endl; } void setValue(size_t index, int value) { if(index < m_size) m_data[index] = value; } void print() const { std::cout << "地址:" << m_data << " 数据:"; for(size_t i=0; i<m_size; ++i) std::cout << " " << m_data[i]; std::cout << std::endl; } }; int main() { SecureArray arr1(3); arr1.setValue(0, 10); arr1.setValue(1, 20); // 场景1:显式拷贝 SecureArray arr2 = arr1; // 调用拷贝构造 // 场景2:修改副本不影响原对象 arr2.setValue(2, 30); // 验证独立性 arr1.print(); // 输出: 地址:0x... 数据:10 20 0 arr2.print(); // 输出: 地址:0x... 数据:10 20 30 return 0; } ``` **输出验证**: ``` 执行深拷贝,分配 12 字节内存 地址:0x55a1a3a12e80 数据:10 20 0 地址:0x55a1a3a12eb0 数据:10 20 30 释放 12 字节内存 释放 12 字节内存 ``` - 两个对象的`m_data`地址不同,证明深拷贝成功 - 修改副本数据不影响原始对象 --- #### **五、必须自定义拷贝构造的典型场景** | 特征类型 | 示例 | 风险说明 | |-------------------------|-------------------------------|--------------------------------| | **动态内存管理** | 类包含`new`分配的指针成员 | 默认浅拷贝导致双重释放 | | **唯一资源句柄** | 文件描述符、数据库连接 | 多个对象操作同一资源引发冲突 | | **引用计数控制** | 智能指针的引用计数管理 | 需要精确控制拷贝时的计数增减 | | **复杂状态对象** | 包含互斥锁(mutex)的线程安全类 | 避免多个对象共享同一锁 | --- #### **六、高级技巧规范** 1. **拷贝赋值运算符**: ```cpp SecureArray& operator=(const SecureArray& other) { if(this != &other) { // 防止自赋值 delete[] m_data; // 释放旧资源 m_size = other.m_size; m_data = new int[m_size]; // 分配新资源 memcpy(m_data, other.m_data, m_size*sizeof(int)); } return *this; } ``` 2. **C++11改进:删除拷贝**: ```cpp SecureArray(const SecureArray&) = delete; // 禁用拷贝构造 SecureArray& operator=(const SecureArray&) = delete; // 禁用拷贝赋值 ``` 3. **移动语义(C++11+)**: ```cpp SecureArray(SecureArray&& other) noexcept // 移动构造 : m_data(other.m_data), m_size(other.m_size) { other.m_data = nullptr; // 转移资源所有权 other.m_size = 0; } ``` --- #### **七、经典问题解答** **Q1:为什么拷贝构造函数的参数必须是const引用?** - 避免无限递归:若参数为值传递,为复制实参需要调用拷贝构造函数,导致无限递归 - 保证原对象不被修改:`const`确保复制过程中原对象状态不变 **Q2:何时编译器会隐式生成拷贝构造函数?** 当满足以下条件时,编译器自动生成默认拷贝构造函数: - 类中没有用户声明的拷贝构造函数 - 所有非静态成员均可被拷贝(如具备可访问的拷贝构造函数) **Q3:如何验证拷贝构造函数被调用?** - 在拷贝构造函数中添加调试输出 - 使用`g++ -fno-elide-constructors`关闭编译器优化(观察完整调用链) --- #### **八、性能优化建议** 1. **尽量使用`const引用传递`对象**: ```cpp void processObject(const SecureArray& arr); // 避免不必要的拷贝 ``` 2. **利用返回值优化(RVO/NRVO)**: ```cpp SecureArray createArray() { return SecureArray(1000); // 编译器可能直接构造到目标内存 } ``` 3. **移动语义优先**: ```cpp SecureArray arr = std::move(existingArray); // 触发移动构造,零拷贝 ``` --- ### 总结 拷贝构造函数是C++资源管理的核心机制之一,其正确实现直接影响程序的稳定性和安全性。通过深拷贝避免资源冲突,结合现代C++的移动语义和`=delete`语法,可以构建更高效、更安全的对象复制策略。理解其工作原理后,开发者能更好地控制对象生命周期,避免内存泄漏和竞态条件等常见问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值