我真的很困惑,我已经看了好几次了,它仍然没有点击。就内存使用而言,复制构造函数和移动构造函数在幕后是什么样?我真的不明白move构造函数是什么“窃取资源”。应该将move构造函数用于动态分配的内存还是堆栈中的内存?
还告诉我,如果我有这样的代码:
void someFunction(Obj obj){
//code
cout << &obj << endl;
}
int main(){
Obj o;
cout << &o << endl;
someFunction(o);
}
将o
复制到obj
。我的印象是,复制会在内存中创建一个新项目,然后将传递的数据复制到该对象的内存地址。因此obj
将在内存中创建一个新空间,并且o
的数据将被复制到其中。但是我得到的o
和obj
的地址完全相同,所以基本上我不知道发生了什么。
答案 0 :(得分:3)
就内存使用而言,复制构造函数和移动构造函数在幕后是什么样?
如果调用 any 构造函数,则意味着正在内存中创建一个新对象。因此, copy 构造函数和 move 构造函数之间的唯一区别是,传递给该构造函数的源对象是否将其成员字段复制为 或移动到新对象中。
我真的不知道move构造函数是什么“窃取资源”。
想象一个对象,该对象包含指向内存中其他位置的某些数据的成员指针。例如,std::string
指向动态分配的字符数据。或std::vector
指向动态分配的数组。或std::unique_ptr
指向另一个对象。
copy 构造函数必须保持源对象不变,因此必须为其分配对象数据的自己的副本。现在,这两个对象都在不同的内存区域中引用同一数据的不同副本(出于本开始讨论的目的,请不要考虑引用计数的数据,例如std::shared_ptr
)。
这就是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
实例。
不是o
或someFunction(Obj &&obj)
的示例,否,因为它们通过引用引用了someFunction(const Obj &obj)
参数 ,因此,没有创建新对象完全没有引用只是现有对象的别名(在后台,它被实现为指向该对象的指针)。将obj
地址操作符应用于引用将返回所引用对象的地址。这就是为什么在这些示例中,您在&
和main()
中看到相同的地址的原因。
我的印象是,复制会在内存中创建一个新项目,然后将传递的数据复制到该对象的内存地址。
从本质上讲,是的。准确地说,它将复制的对象的成员字段的 values 值复制到新对象的相应成员字段中。
因此
someFunction()
将在内存中创建一个新空间,并将obj
的数据复制到其中。
只有o
是obj
的副本,是的。
答案 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, ]
这里我们只是打印原始对象。
现在,我们通过复制第一个对象来创建第二个对象。这将调用Myclass
的副本构造函数。两个对象中的数据与预期的相同,但它们是副本,因为两个对象中的内存地址不同和。
我们创建了另一个对象,但是现在我们移动 obj1
到这个新对象。这将调用我们类的move构造函数。将一个对象从{em>移出后,就像obj1
一样,除非我们为其分配了新的值,否则我们不应再次使用它。现在obj1.v
中的内部指针是一个空指针,请注意obj3.v
存储自己的数据的地址指向obj1.v
之前指向的位置。这就是将数据从一个对象移动到另一个对象的意思。
但并非所有事物都能被移动。请注意,复制了整数a
成员,而移动了向量v
成员。尽管int
的复制成本很低,但有时即使复制成本也不低的数据也无法移动。例如,如果我们向double arr[100]
添加Myclass
数据,则移动构造函数仍会将obj1.arr
复制到新的obj3
对象,因为arr
在堆栈。