编程C++ 中级开发者

解释C++中浅拷贝和深拷贝之间的区别,以动态内存的容器为例。如何手动实现深拷贝?

用 Hintsage AI 助手通过面试

答案。

C++中的对象拷贝机制分为浅拷贝(shallow copy)和深拷贝(deep copy)。对于具有动态分配内存的类,这种差异尤为重要。

问题背景

在C++中,许多数据结构使用动态内存(new/delete)。默认情况下,编译器会生成一个执行逐字节拷贝的拷贝构造函数和赋值运算符(shallow copy)。这种方法速度快,但如果对象管理外部资源,则会很危险。

问题

浅拷贝仅拷贝动态分配资源的地址。当删除一个对象时,内存将被释放,而另一个实例将保留“悬空”(dangling)指针。因此,会导致双重删除、内存泄漏和崩溃。

解决方案

深拷贝需要显式地创建所有动态资源的副本。为此,需要在类中手动实现拷贝构造函数和赋值运算符,以保证每个元素的拷贝。

以下是一个包含数组的类的代码示例:

class DynArray { int* data; size_t size; public: DynArray(size_t n) : size(n), data(new int[n]) {} ~DynArray() { delete[] data; } // 深拷贝构造函数 DynArray(const DynArray& other) : size(other.size), data(new int[other.size]) { for (size_t i = 0; i < size; ++i) data[i] = other.data[i]; } // 深拷贝赋值运算符 DynArray& operator=(const DynArray& other) { if (this != &other) { delete[] data; size = other.size; data = new int[size]; for (size_t i = 0; i < size; ++i) data[i] = other.data[i]; } return *this; } };

关键特性:

  • 浅拷贝拷贝指针,深拷贝创建新的动态内存实例。
  • 深拷贝需要在构造函数和赋值运算符中实现自己的拷贝逻辑。
  • 忽视深拷贝的必要性会导致难以调试的错误。

反向问答。

编译器总是正确生成拷贝构造函数和赋值运算符,对吗?

答案:

错误。对于具有动态资源的类,默认的拷贝是不正确的:两个对象将共享同一资源。需要在拥有外部资源时显式实现深拷贝。

如果只实现了深拷贝构造函数/赋值运算符,是否需要实现析构函数?

答案:

是的,否则会导致内存泄漏:如果在用户的拷贝构造函数中释放内存,但没有实现析构函数,内存将不会在对象销毁时被释放。

std::vector可以存储指针吗?为什么在其拷贝时可能会发生内存泄漏?

答案:

可以,std::vector可以安全地存储指针。在拷贝这样的std::vector时,拷贝的是指针本身,而不是它们所指向的对象。这是浅拷贝:如果需要深拷贝所有内容,则需要手动拷贝每个对象并在内存中独立存放。

示例:

std::vector<int*> v1; v1.push_back(new int(42)); std::vector<int*> v2 = v1; // 拷贝的是指针,而不是 *int

常见错误和反模式

  • 忽视实现“三个法则”的必要性。
  • 拷贝指针,以为这是对象的拷贝。
  • 不在析构函数中释放动态资源。
  • 对于拥有资源的类使用浅拷贝。

生活中的例子

消极案例

程序员实现了一个数组的包装类,但没有重写拷贝构造函数/赋值运算符。结果,两个对象拥有同一块内存,销毁一个时会导致访问另一个时崩溃。

优点:

  • 运行速度快(没有拷贝)。

缺点:

  • 运行时错误难以调试;存在双重释放/段错误。

积极案例

开发者实现了深拷贝:拷贝数组的内容,拥有自己的析构函数和防止自我赋值的赋值运算符。

优点:

  • 安全的拷贝和内存释放。
  • 代码可维护且可扩展。

缺点:

  • 稍微多一点代码和内存开销。
  • 对于拥有多个动态资源的类来说更复杂。