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

时间:2020-10-15 19:47:56

标签: c++

我真的很困惑,我已经看了好几次了,它仍然没有点击。就内存使用而言,复制构造函数和移动构造函数在幕后是什么样?我真的不明白move构造函数是什么“窃取资源”。应该将move构造函数用于动态分配的内存还是堆栈中的内存?

还告诉我,如果我有这样的代码:

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

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

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

2 个答案:

答案 0 :(得分:3)

就内存使用而言,复制构造函数和移动构造函数在幕后是什么样?

如果调用 any 构造函数,则意味着正在内存中创建一个新对象。因此, copy 构造函数和 move 构造函数之间的唯一区别是,传递给该构造函数的源对象是否将其成员字段复制为 移动到新对象中。

我真的不知道move构造函数是什么“窃取资源”。

想象一个对象,该对象包含指向内存中其他位置的某些数据的成员指针。例如,std::string指向动态分配的字符数据。或std::vector指向动态分配的数组。或std::unique_ptr指向另一个对象。

copy 构造函数必须保持源对象不变,因此必须为其分配对象数据的自己的副本。现在,这两个对象都在不同的内存区域中引用同一数据的不同副本(出于本开始讨论的目的,请不要考虑引用计数的数据,例如std::shared_ptr)。

另一方面, move 构造函数可以通过获取引用该数据的指针的所有权来简单地“移动”数据,而将数据本身保留在其所在的位置。现在,新对象指向原始数据,而源对象被修改为不再指向数据。数据本身保持不变。

这就是move semantics复制/值语义学更有效的原因。

下面是一个演示此情况的示例:

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

是否应该将move构造函数用于动态分配的内存或堆栈上的内存?

移动语义最常与动态资源的指针/句柄一起使用,是的(是的,但是在其他情况下,移动语义可能会有用)。更新指向数据的指针比制作数据的副本要快。知道源对象将不再需要引用其数据,因此无需复制数据然后销毁原始对象,就可以将原始对象按原样“移动”到源对象到目标对象。

“移动”数据是POD数据(纯旧数据,即整数,浮点小数,布尔值,结构/数组聚合等)时,移动语义无助于提高效率的地方。 “移动”此类数据与“复制”相同。例如,您不能将int“移动”到另一个int,只能复制其值。

还有人告诉我,如果我有这样的代码:... o将被复制到obj

someFunction(Obj obj)的示例中,是的,因为它采用其obj参数按值 ,因此调用了{{的 copy 1}},从Obj创建obj实例。

不是osomeFunction(Obj &&obj)的示例,否,因为它们通过引用引用了someFunction(const Obj &obj)参数 ,因此,没有创建新对象完全没有引用只是现有对象的别名(在后台,它被实现为指向该对象的指针)。将obj地址操作符应用于引用将返回所引用对象的地址。这就是为什么在这些示例中,您在&main()中看到相同的地址的原因。

我的印象是,复制会在内存中创建一个新项目,然后将传递的数据复制到该对象的内存地址。

从本质上讲,是的。准确地说,它将复制的对象的成员字段的 values 值复制到新对象的相应成员字段中。

因此someFunction()将在内存中创建一个新空间,并将obj的数据复制到其中。

只有oobj副本,是的。

答案 1 :(得分:0)

雷米(Remy)的回答非常好,这里还涉及其他相关问题。但是,如果对您来说答案仍然看起来有些“抽象”,那么最好的方法就是亲自观察实际情况。考虑下面的课程。

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, ]

第1部分

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

第2部分

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

我们创建了另一个对象,但是现在我们移动 obj1到这个新对象。这将调用我们类的move构造函数。将一个对象从{em>移出后,就像obj1一样,除非我们为其分配了新的值,否则我们不应再次使用它。现在obj1.v中的内部指针是一个空指针,请注意obj3.v存储自己的数据的地址指向obj1.v之前指向的位置。这就是将数据从一个对象移动到另一个对象的意思。

但并非所有事物都能被移动。请注意,复制了整数a成员,而移动了向量v成员。尽管int的复制成本很低,但有时即使复制成本也不低的数据也无法移动。例如,如果我们向double arr[100]添加Myclass数据,则移动构造函数仍会将obj1.arr复制到新的obj3对象,因为arr在堆栈。

相关问题