C++拷贝构造函数和拷贝赋值运算符

在 C++ 中,拷贝构造函数(Copy Constructor)和 拷贝赋值运算符(Copy Assignment Operator)是两个重要的特殊成员函数,用于控制对象的拷贝行为,文章将详细介绍它们的应用场景。

欢迎进入我的哔哩哔哩频道进行学习!

我们可以通过一个自定义的类来详细说明拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符的用法。这次我们用一个简单的 Buffer类来管理动态分配的数组。


1. 拷贝构造函数

如果一个类的构造函数的第一个参数是所属的类类型引用,若有额外的参数,那么这些额外的参数都有默认值。该构造函数的默认参数必须放在函数声明中,除非该构造函数没有函数声明,那么这个构造函数就叫拷贝构造函数。这个拷贝构造函数会在一定的时机被系统自动调用。

拷贝构造函数用于创建一个新对象,并将其初始化为另一个同类型对象的副本。它会深拷贝资源。

定义

拷贝构造函数用于通过同类型的另一个对象初始化新对象,其形式为:

复制代码

ClassName(const ClassName& other);

调用场景

拷贝构造函数会在以下情况下被调用:

  1. 显式初始化新对象

    复制代码

    MyClass obj1;
    MyClass obj2(obj1); // 调用拷贝构造函数
    MyClass obj3 = obj1; // 初始化时赋值,调用拷贝构造函数
    MyClass obj4{obj1}; // 初始化时赋值,调用拷贝构造函数
    MyClass obj5 = {obj1}; // 初始化时赋值,调用拷贝构造函数
  2. 对象作为函数参数按值传递

    复制代码

    void func(MyClass obj); // 调用时参数拷贝构造
  3. 对象作为函数返回值按值返回(可能被优化,不总调用):

    复制代码

    MyClass func() {
    MyClass obj;
    return obj; // 可能调用拷贝构造函数(视编译器优化而定)
    }
  4. 容器操作(如std::vectorpush_back):

    复制代码

    std::vector<MyClass> vec;
    MyClass obj;
    vec.push_back(obj); // 将 obj 拷贝到容器中

应用场景

  • 当需要深拷贝资源(如动态内存、文件句柄)时,必须实现拷贝构造函数。
  • 如果类需要严格的拷贝控制,避免浅拷贝导致的资源重复释放。

示例代码

#include <iostream>

class Buffer
{
public:
// 普通构造函数
Buffer(size_t size) : m_size(size)
{
std::cout << "普通构造函数" << std::endl;
m_data = new int[m_size];
for (size_t i = 0; i < m_size; ++i)
{
m_data[i] = i; // 初始化数据
}
}

// 拷贝构造函数
Buffer(const Buffer& other) : m_size(other.m_size)
{
std::cout << "拷贝构造函数" << std::endl;
m_data = new int[m_size];
for (size_t i = 0; i < m_size; ++i)
{
m_data[i] = other.m_data[i]; // 深拷贝数据
}
}

// 析构函数
~Buffer()
{
std::cout << "析构函数" << std::endl;
delete[] m_data;
}

// 打印数据
void print() const
{
for (size_t i = 0; i < m_size; ++i)
{
std::cout << m_data[i] << " ";
}
std::cout << std::endl;
}

private:
int* m_data;
size_t m_size;
};

int main() {
Buffer b1(5); // 调用普通构造函数
Buffer b2 = b1; // 调用拷贝构造函数

std::cout << "b1: ";
b1.print(); // 输出: 0 1 2 3 4
std::cout << "b2: ";
b2.print(); // 输出: 0 1 2 3 4

return 0;
}

输出

普通构造函数
拷贝构造函数
b1: 0 1 2 3 4
b2: 0 1 2 3 4
析构函数
析构函数

2. 拷贝赋值运算符

拷贝赋值运算符用于将一个对象的内容赋值给另一个已经存在的对象。它也会深拷贝资源。

定义

拷贝赋值运算符用于将一个已存在对象的值赋值给另一个已存在对象,其形式为:

复制代码

ClassName& operator=(const ClassName& other);

调用场景

拷贝赋值运算符在以下情况被调用:

  1. 显式赋值操作

    复制代码

    MyClass obj1;
    MyClass obj2;
    obj2 = obj1; // 调用拷贝赋值运算符
  2. 链式赋值

    复制代码

    MyClass obj1, obj2, obj3;
    obj3 = obj2 = obj1; // 链式调用拷贝赋值运算符
  3. 对象通过现有对象更新值

    复制代码

    MyClass obj1;
    obj1 = MyClass(); // 临时对象赋值(可能触发移动语义优化)

应用场景

  • 需要处理自我赋值(Self-Assignment)的场景(如 obj = obj;),避免资源泄漏。
  • 实现深拷贝的赋值操作,确保资源被正确释放和重新分配。

示例代码

#include <iostream>

class Buffer {
private:
int* data;
size_t size;

public:
// 普通构造函数
Buffer(size_t size) : size(size) {
std::cout << "普通构造函数" << std::endl;
data = new int[size];
for (size_t i = 0; i < size; ++i) {
data[i] = i; // 初始化数据
}
}

// 拷贝构造函数
Buffer(const Buffer& other) : size(other.size) {
std::cout << "拷贝构造函数" << std::endl;
data = new int[size];
for (size_t i = 0; i < size; ++i) {
data[i] = other.data[i]; // 深拷贝数据
}
}

// 拷贝赋值运算符
Buffer& operator=(const Buffer& other) {
std::cout << "拷贝赋值运算符" << std::endl;
if (this == &other) {
return *this; // 自赋值检查
}
delete[] data; // 释放原有资源
size = other.size;
data = new int[size];
for (size_t i = 0; i < size; ++i) {
data[i] = other.data[i]; // 深拷贝数据
}
return *this;
}

// 析构函数
~Buffer() {
std::cout << "析构函数" << std::endl;
delete[] data;
}

// 打印数据
void print() const {
for (size_t i = 0; i < size; ++i) {
std::cout << data[i] << " ";
}
std::cout << std::endl;
}
};

int main() {
Buffer b1(5); // 调用普通构造函数
Buffer b2(3); // 调用普通构造函数

b2 = b1; // 调用拷贝赋值运算符

std::cout << "b1: ";
b1.print(); // 输出: 0 1 2 3 4
std::cout << "b2: ";
b2.print(); // 输出: 0 1 2 3 4

return 0;
}

输出

普通构造函数
普通构造函数
拷贝赋值运算符
b1: 0 1 2 3 4
b2: 0 1 2 3 4
析构函数
析构函数

总结

  • 拷贝构造函数拷贝赋值运算符用于深拷贝资源。

关键区别

特性 拷贝构造函数 拷贝赋值运算符
目的 初始化新对象 修改已存在对象的值
调用时机 对象初始化时 赋值操作时(=
资源处理 构造新资源 释放旧资源后构造新资源
函数签名 ClassName(const ClassName&) ClassName& operator=(const ClassName&)

规则和最佳实践

  1. Rule of Three:如果类需要手动实现析构函数、拷贝构造函数或拷贝赋值运算符中的一个,通常需要同时实现另外两个。
  2. 自我赋值检查:在拷贝赋值运算符中,应检查 this != &other 以避免自我赋值的资源问题。
  3. 深拷贝与浅拷贝:如果类管理资源(如动态内存),必须实现深拷贝;否则默认浅拷贝可能导致未定义行为。

通过合理实现这两个函数,可以确保对象的拷贝行为符合预期,避免资源泄漏和逻辑错误。