C++中的复制构造函数和移动构造函数有什么区别

a11xaf1n  于 2023-05-02  发布在  其他
关注(0)|答案(2)|浏览(131)

我真的很困惑,我已经查了几次,它仍然没有点击。复制构造函数和移动构造函数在后台的内存使用情况如何?我真的不明白什么是“窃取资源”与移动构造函数。移动构造函数应该用于动态分配的内存还是堆栈上的内存?
我还被告知,如果我有这样的代码:

void someFunction(Obj obj){
    //code
    cout << &obj << endl;
}

int main(){
    Obj o;
    cout << &o << endl;
    someFunction(o);
}

o将被复制到obj。我的印象是,复制在内存中创建一个新项,并将传递的数据复制到该对象的内存地址。因此obj将在内存中创建一个新空间,o的数据将复制到该空间。但是我得到的oobj的地址完全相同,所以基本上我不知道发生了什么。

c0vxltue

c0vxltue1#

复制构造函数和移动构造函数在后台的内存使用情况如何?
如果调用了 any 构造函数,则意味着在内存中创建了一个新对象。因此,copy 构造函数和 move 构造函数之间的唯一区别是传递给构造函数的源对象是否将其成员字段 * 复制 * 或 * 移动 * 到新对象中。
我真的不明白什么是“窃取资源”与移动构造函数。
假设一个对象包含一个成员指针,指向内存中其他位置的一些数据。例如,std::string指向动态分配的字符数据。或者一个指向动态分配数组的std::vector。或者一个std::unique_ptr指向另一个对象。

  • copy* 构造函数必须保持源对象的完整性,因此它必须为自己分配对象数据的副本。现在,两个对象都引用了内存不同区域中相同数据的不同副本(出于开始讨论的目的,我们不要考虑引用计数数据,如std::shared_ptr)。

另一方面,move 构造函数可以通过获取指向数据的指针的所有权来简单地“移动”数据,而将数据本身留在其所在的位置。新对象现在指向原始数据,源对象被修改为不再指向该数据。数据本身保持不变。
这就是为什么move semanticscopy/value sematics 更高效。
下面是一个例子来说明这一点:

class MyIntArray
{
private:
    int *arr = nullptr;
    int size = 0;

public:
    MyIntArray() = default;

    MyIntArray(int size) {
        arr = new int[size];
        this->size = size;
        for(int i = 0; i < size; ++i) {
            arr[i] = i;
        }
    }

    // copy constructor
    MyIntArray(const MyIntArray &src) {
        // allocate a new copy of the array...
        arr = new int[src.size];
        size = src.size;
        for(int i = 0; i < src.size; ++i) {
            arr[i] = src.arr[i];
        }
    }

    // move constructor
    MyIntArray(MyIntArray &&src) {
        // just swap the array pointers...
        src.swap(*this);
    }

    ~MyIntArray() {
        delete[] arr;
    }

    // copy assignment operator
    MyIntArray& operator=(const MyIntArray &rhs) {
        if (&rhs != this) {
            MyIntArray temp(rhs); // copies the array
            temp.swap(*this);
        }
        return *this;
    }

    // move assignment operator
    MyIntArray& operator=(MyIntArray &&rhs) {
        MyIntArray temp(std::move(rhs)); // moves the array
        temp.swap(*this);
        return *this;
    }

    /*
    or, the above 2 operators can be implemented as 1 operator, like below.
    This allows the caller to decide whether to construct the rhs parameter
    using its copy constructor or move constructor...

    MyIntArray& operator=(MyIntArray rhs) {
        rhs.swap(*this);
        return *this;
    }
    */

    void swap(MyIntArray &other) {
        // swap the array pointers...
        std::swap(arr, other.arr);
        std::swap(size, other.size);
    }
};
void copyArray(const MyIntArray &src)
{
    MyIntArray arr(src); // copies the array
    // use arr as needed...
}

void moveArray(MyIntArray &&src)
{
    MyIntArray arr(std::move(src)); // moved the array
    // use arr as needed...
}

MyIntArray arr1(5);                // creates a new array
MyIntArray arr2(arr1);             // copies the array
MyIntArray arr3(std::move(arr2));  // moves the array
MyIntArray arr4;                   // default construction
arr4 = arr3;                       // copies the array
arr4 = std::move(arr3);            // moves the array
arr4 = MyIntArray(1);              // creates a new array and moves it

copyArray(arr4);                   // copies the array
moveArray(std::move(arr4));        // moves the array

copyArray(MyIntArray(10));         // creates a new array and copies it
moveArray(MyIntArray(10));         // creates a new array and moves it

移动构造函数应该用于动态分配的内存还是堆栈上的内存?
移动语义 * 最常 * 用于指向动态资源的指针/句柄,是的(但也有其他情况下移动语义可能有用)。更新指向数据的指针比复制数据要快。知道源对象将不再需要引用其数据,不需要复制数据然后销毁原始数据,原始数据可以按原样从源对象“移动”到目标对象。
当被“移动”的数据是POD数据(普通的旧数据,即整数、浮点小数、布尔值、结构/数组聚合等)时,移动语义无助于提高效率。“移动”这样的数据与“复制”它是相同的。例如,您不能将一个int“移动”到另一个int,您只能复制它的值。
我还被告知,如果我有这样的代码:... o将被复制到obj
someFunction(Obj obj)的例子中,是的,因为它接受obj参数 by value,从而调用Objcopy 构造函数从o创建obj示例。
someFunction(Obj &&obj)someFunction(const Obj &obj)的例子中没有,因为它们采用obj参数 by reference,因此根本没有创建新对象。引用只是现有对象的别名(在幕后,它被实现为对象的指针)。对引用应用& address-of运算符将返回被引用对象的地址。这就是为什么在这些示例中,main()someFunction()中打印了相同的地址。
我的印象是,复制在内存中创建一个新项,并将传递的数据复制到该对象的内存地址。
基本上是的更准确地说,它将传递对象的成员字段的 * 值 * 复制到新对象的相应成员字段。
因此obj将在内存中创建一个新空间,o的数据将复制到该空间。
只有当objo的 * 副本 * 时,才是。

h43kikqp

h43kikqp2#

雷米的回答很好,这里还有其他相关的问题。但如果答案对你来说仍然有些“抽象”,那么最好的方法就是自己看看到底发生了什么。考虑下面的类。

void print_vec(auto v) {
    std::cout << "[";
    for(auto elem : v) {
        std::cout << elem << ", ";
    }
    std::cout << "]" << std::endl;
}

class Myclass {
public:
    // Public data to make inspection easy in this example
    int a;
    std::vector<int> v;

    Myclass(int pa, std::vector<int> pv) : a(pa), v(std::move(pv)) {}

    void print(std::string_view label) {
        std::cout << label << " object\n    a stored in " << &a << " with value " << a << "\n";
        std::cout << "    v elements stored in " << v.data() << " with value ";
        print_vec(v);
        std::cout << std::endl;
    }
};

int main(int argc, char *argv[])
{
    Myclass obj1(10, {1,2,3,4,5});

    std::cout << "xxxxxxxxxx Part 1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" << std::endl;
    obj1.print("obj1");

    std::cout << "xxxxxxxxxx Part 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" << std::endl;
    // Now let's create a copy -> This calls the copy constructor
    auto obj2 = obj1;

    obj1.print("obj1");
    obj2.print("obj2");

    std::cout << "xxxxxxxxxx Part 3 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" << std::endl;
    // Now let's call the move constructor
    auto obj3 = std::move(obj1);

    obj1.print("obj1");
    obj3.print("obj3");

    return 0;
}

Myclass必须是数据成员。一个整数和一个std::vector如果你运行这个代码,你会得到如下的结果

xxxxxxxxxx Part 1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
obj1 object
    'a' stored in 0x7ffd1c8de0f0 with value 10
    'v' elements stored in 0x55946fab8eb0 with value [1, 2, 3, 4, 5, ]

xxxxxxxxxx Part 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
obj1 object
    'a' stored in 0x7ffd1c8de0f0 with value 10
    'v' elements stored in 0x55946fab8eb0 with value [1, 2, 3, 4, 5, ]

obj2 object
    'a' stored in 0x7ffd1c8de110 with value 10
    'v' elements stored in 0x55946fab92e0 with value [1, 2, 3, 4, 5, ]

xxxxxxxxxx Part 3 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
obj1 object
    'a' stored in 0x7ffd1c8de0f0 with value 10
    'v' elements stored in 0 with value []

obj3 object
    'a' stored in 0x7ffd1c8de130 with value 10
    'v' elements stored in 0x55946fab8eb0 with value [1, 2, 3, 4, 5, ]

第一部分

这里我们只是打印原始对象。

第二部分

现在我们通过复制第一个对象来创建第二个对象。这将调用Myclass的复制构造函数。两个对象中的数据与预期相同,但它们是副本,因为内存地址在对象和``中不同

第三部分

我们创建了另一个对象,但现在我们将obj1移动到这个新对象。这将调用我们类的move构造函数。一旦一个对象从 * 移动,就像obj1一样,我们不应该再使用它,除非我们给它赋值。现在,obj1.v中的内部指针是一个空指针,请注意,obj3.v存储自己数据的地址指向obj1.v之前指向的地址。这就是将数据从一个对象移动到另一个对象的意思。
但并不是所有的东西都能被移动。请注意,复制了整数a成员,而移动了向量v成员。虽然int的复制成本很低,但有时即使是复制成本不低的数据也无法移动。例如,如果我们将double arr[100]数据添加到Myclass,那么移动构造函数仍然会将obj1.arr复制到新的obj3对象,因为arr在堆栈中。

相关问题