c++中的复制(深拷贝/浅拷贝/拷贝构造函数/拷贝赋值运算符/移动语义)

zengchenAAA 2024-06-26 09:05:04 阅读 97

c++中的复制

在C++中,复制指的是将一个对象的内容复制到另一个对象中。复制可以通过拷贝构造函数、拷贝赋值运算符或移动语义来实现。

预备知识

在C++中,拷贝构造函数和赋值操作符可以执行浅拷贝或深拷贝。这两种拷贝方式涉及到对象中成员变量的复制,特别是对于动态分配的资源(比如堆上的内存)的处理。

浅拷贝(Shallow Copy)

浅拷贝只复制对象中成员变量的值,而不会创建新的资源副本。如果对象中有指向堆内存的指针,浅拷贝仅复制指针的值,而不复制指针指向的实际数据。这意味着两个对象将共享同一块内存,这可能会导致问题:

当一个对象的析构函数调用 delete 释放了内存,另一个对象也会受到影响,因为它们共享相同的指针。当一个对象改变了堆内存中的数据,另一个对象也会受到影响,因为它们指向相同的内存位置。

深拷贝(Deep Copy)

深拷贝创建了一个新的资源副本,而不仅仅是复制指针的值。这意味着每个对象都有自己的独立内存副本,彼此之间不会相互影响。对于指向堆内存的指针,深拷贝会为每个对象分配新的内存,并将原始数据复制到新的内存中。

下面是一个示例,演示了浅拷贝和深拷贝的区别:

#include <iostream>

#include <cstring>

class MyString {

private:

char* buffer;

public:

// 构造函数

MyString(const char* initialInput) {

if (initialInput != nullptr) {

buffer = new char[strlen(initialInput) + 1];

strcpy(buffer, initialInput);

} else {

buffer = nullptr;

}

}

// 拷贝构造函数(浅拷贝)

MyString(const MyString& other) {

buffer = other.buffer; // 浅拷贝,只复制指针的值

}

// 拷贝构造函数(深拷贝)

//MyString(const MyString& other) {

// if (other.buffer != nullptr) {

// buffer = new char[strlen(other.buffer) + 1];

// strcpy(buffer, other.buffer);

// } else {

// buffer = nullptr;

// }

//}

// 析构函数

~MyString() {

delete[] buffer;

}

// 打印字符串

void Print() {

if (buffer != nullptr) {

std::cout << buffer;

} else {

std::cout << "(null)";

}

std::cout << std::endl;

}

};

int main() {

MyString str1("Hello");

MyString str2 = str1; // 调用拷贝构造函数

// 修改str1

str1.Print(); // 输出 "Hello"

str2.Print(); // 输出 "Hello"

delete[] str1; // 删除str1的buffer

str1.Print(); // 输出 "(null)"

str2.Print(); // 输出 "(null)"

return 0;

}

在上述示例中,我们有一个简化的 MyString 类,它具有一个 char 指针 buffer 来存储字符串。我们定义了两个拷贝构造函数:一个执行浅拷贝,另一个执行深拷贝(被注释掉了)。

当我们使用浅拷贝构造函数时,str1str2 共享相同的 buffer,这意味着当我们删除 str1buffer 后,str2buffer 也指向已经被删除的内存位置。这会导致未定义的行为。而如果我们使用深拷贝构造函数,每个对象都有自己的 buffer,因此删除 str1buffer 不会影响 str2。 因此,深拷贝通常是更安全和可靠的选择,特别是当对象包含指向动态分配内存的指针时。

1. 拷贝构造函数(Copy Constructor)

拷贝构造函数是一种特殊的成员函数,用于创建一个新对象,并将另一个对象的内容复制到新对象中。当对象以值传递的方式传递给函数、通过值返回或者通过赋值操作符进行复制时,拷贝构造函数将被调用。它的声明形式为 ClassName(const ClassName& other),其中 ClassName 是类的名称,other 是传递给拷贝构造函数的另一个同类对象的引用。

拷贝构造函数用于创建一个新对象,并将另一个对象的内容复制到新对象中。它在以下情况下被调用:

当对象以值传递的方式传递给函数当对象通过值返回当对象通过赋值操作符进行复制

拷贝构造函数在C++中是默认提供的。如果你没有显式地定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数。这个默认的拷贝构造函数执行的是浅拷贝操作,即简单地复制对象的每个成员变量的值。

默认的拷贝构造函数的形式为 ClassName(const ClassName&),其中 ClassName 是类的名称。它通常与赋值操作符的形式相似,但它的目的是将一个现有对象的内容复制到新创建的对象中,而不是修改已有对象的内容。

虽然默认的拷贝构造函数在许多情况下可以正常工作,但在特定情况下,你可能需要自定义拷贝构造函数来执行更复杂的操作,比如深拷贝或资源管理。在这种情况下,你可以通过显式地定义自己的拷贝构造函数来覆盖默认行为。

下面是拷贝构造函数的例子:

#include <iostream>

class MyClass {

private:

int value;

public:

// 拷贝构造函数

MyClass(const MyClass& other) {

std::cout << "Copy constructor called" << std::endl;

value = other.value; // 复制另一个对象的值

}

// 构造函数

MyClass(int val) : value(val) { }

// 打印值

void printValue() {

std::cout << "Value: " << value << std::endl;

}

};

int main() {

MyClass obj1(10);

MyClass obj2 = obj1; // 调用拷贝构造函数

obj1.printValue(); // 输出 "Value: 10"

obj2.printValue(); // 输出 "Value: 10"

return 0;

}

2. 拷贝赋值运算符(Copy Assignment Operator)

拷贝赋值运算符用于将一个对象的内容复制到另一个已经存在的对象中。它通常被重载为类成员函数,并采用形式如 ClassName& operator=(const ClassName& other) 的声明,其中 ClassName 是类的名称,other 是要复制的对象的引用。拷贝赋值运算符常用于实现对象的赋值操作,例如 obj1 = obj2

#include <iostream>

class MyClass {

private:

int value;

public:

// 构造函数

MyClass(int val) : value(val) { }

// 拷贝赋值运算符

MyClass& operator=(const MyClass& other) {

std::cout << "Copy assignment operator called" << std::endl;

if (this != &other) { // 避免自我赋值

value = other.value; // 复制另一个对象的值

}

return *this;

}

// 打印值

void printValue() {

std::cout << "Value: " << value << std::endl;

}

};

int main() {

MyClass obj1(10);

MyClass obj2(20);

obj2 = obj1; // 调用拷贝赋值运算符

obj1.printValue(); // 输出 "Value: 10"

obj2.printValue(); // 输出 "Value: 10"

return 0;

}

在C++中,如果你没有显式地定义拷贝赋值运算符(operator=),编译器会为你生成一个默认的拷贝赋值运算符。这个默认的拷贝赋值运算符执行的是浅拷贝(member-wise copy),它将一个对象的每个成员变量的值复制到另一个对象中。默认的拷贝赋值运算符的声明形式为 ClassName& operator=(const ClassName&),其中 ClassName 是类的名称。

和默认的拷贝构造函数一样,虽然默认的拷贝赋值运算符在许多情况下可以正常工作,但在特定情况下你可能需要自定义拷贝赋值运算符来执行更复杂的操作,比如深拷贝、资源管理或其他逻辑。在这种情况下,你可以通过显式地定义自己的拷贝赋值运算符来覆盖默认行为。

需要注意的是,如果类中包含了动态分配的资源(比如指针),默认的拷贝赋值运算符执行的是浅拷贝,可能会导致资源重复释放或悬挂指针等问题。因此,当类需要进行资源管理时,通常需要显式地定义拷贝赋值运算符,以确保正确地管理资源。

3. 移动语义(Move Semantics)

移动语义是C++11引入的一个重要概念,旨在提高对象的传递和赋值的效率。移动操作是一种资源转移操作,将一个对象的资源所有权从一个对象转移到另一个对象,而不是复制资源。移动语义通过移动构造函数和移动赋值运算符来实现。移动构造函数的声明形式为 ClassName(ClassName&& other),移动赋值运算符的声明形式为 ClassName& operator=(ClassName&& other),其中 ClassName 是类的名称,other 是右值引用。

这里牵扯到右值引用,如果不动的小伙伴可以参考这篇文章:cpp中的右值引用(&&)及其相关拓展知识

#include <iostream>

class MyResource {

public:

MyResource() { std::cout << "Resource acquired" << std::endl; }

~MyResource() { std::cout << "Resource released" << std::endl; }

};

class MyClass {

private:

MyResource* resource;

public:

// 构造函数

MyClass() : resource(new MyResource()) { }

// 移动构造函数

MyClass(MyClass&& other) noexcept : resource(other.resource) {

std::cout << "Move constructor called" << std::endl;

other.resource = nullptr; // 避免资源重复释放

}

// 移动赋值运算符

MyClass& operator=(MyClass&& other) noexcept {

std::cout << "Move assignment operator called" << std::endl;

if (this != &other) { // 避免自我赋值

delete resource; // 释放当前对象的资源

resource = other.resource; // 转移资源所有权

other.resource = nullptr; // 避免资源重复释放

}

return *this;

}

// 打印资源状态

void printResourceStatus() {

if (resource != nullptr) {

std::cout << "Resource exists" << std::endl;

} else {

std::cout << "No resource" << std::endl;

}

}

};

int main() {

MyClass obj1;

MyClass obj2;

obj2 = std::move(obj1); // 调用移动赋值运算符

obj1.printResourceStatus(); // 输出 "No resource"

obj2.printResourceStatus(); // 输出 "Resource exists"

return 0;

}

从C++11开始,标准库引入了移动语义和右值引用的概念,以支持资源管理的优化和性能提升。

默认情况下,如果你没有显式地定义移动构造函数和移动赋值运算符,编译器不会为你自动生成这些函数,因此你需要自己编写它们来实现移动语义。 移动构造函数的形式为 ClassName(ClassName&& other),移动赋值运算符的形式为 ClassName& operator=(ClassName&& other),其中 ClassName 是类的名称。

移动语义允许在对象之间转移资源所有权而不是复制资源,这对于大型对象或包含大量资源(比如动态分配的内存或文件句柄)的对象来说,可以显著提高性能和效率。因此,在需要对资源进行移动而不是复制时,你应该自己编写移动构造函数和移动赋值运算符来利用移动语义。



声明

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