看一段代码,相关:构造函数、拷贝/移动构造函数、vector扩容、智能指针
示例
如果 std::vector
中存储的是自定义类型对象,以下代码可能会导致问题:
#include <vector>
#include <iostream>
class MyClass {
public:
int* data;
MyClass(int value) : data(new int(value)) {}
~MyClass() {
delete data;
}
// 忘记实现拷贝构造函数或移动构造函数
};
int main() {
std::vector<MyClass> vec;
vec.reserve(10); // 预分配空间
for (int i = 0; i < 5; ++i) {
vec.emplace_back(i); // 可能会导致问题
}
return 0;
}
原因:
MyClass
的析构函数释放了动态内存,但拷贝构造函数和移动构造函数未正确实现,导致扩容时拷贝或移动对象后,多个对象共享同一块内存。- 在释放旧空间时,会多次释放同一块内存,导致崩溃。
解决方案:
- 正确实现拷贝构造函数和移动构造函数,或者使用智能指针。
在上述代码中,std::vector<MyClass> vec;
是一个以 MyClass
为元素类型的容器,在执行 vec.emplace_back(i);
时是否调用拷贝构造函数还是移动构造函数取决于 MyClass
是否支持移动语义 和 编译器的优化行为。
emplace_back(i)
的行为
vec.emplace_back(i)
会直接在 std::vector
已分配的内存中构造一个新的 MyClass
对象,而不会调用拷贝构造函数或移动构造函数。它通过传递 i
调用适配的构造函数。在你的例子中,它调用了 MyClass(int value)
构造函数。
当扩容发生时
当 std::vector
需要扩容时(例如,超过当前容量),会分配一块新的更大的连续内存,并将旧内存中的元素移动到新内存中。这时会发生拷贝或移动构造。
- 如果
MyClass
支持移动构造函数,则会优先调用移动构造函数。 - 如果
MyClass
没有定义移动构造函数,但支持拷贝构造函数,则会调用拷贝构造函数。 - 如果
MyClass
同时不支持拷贝构造和移动构造,则会报编译错误。
编译器如何选择
1. 移动构造函数优先
如果 MyClass
定义了移动构造函数(或者编译器能够生成一个隐式移动构造函数),那么扩容时 std::vector
会调用移动构造函数。例如:
class MyClass {
public:
int* data;
MyClass(int value) : data(new int(value)) {}
~MyClass() { delete data; }
// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr;
}
// 禁用拷贝构造函数
MyClass(const MyClass&) = delete;
};
int main() {
std::vector<MyClass> vec;
vec.emplace_back(1); // 调用构造函数
vec.emplace_back(2); // 可能扩容时调用移动构造函数
}
2. 没有移动构造函数时
如果没有定义移动构造函数但定义了拷贝构造函数,或者编译器无法隐式生成移动构造函数,则会调用拷贝构造函数。例如:
class MyClass {
public:
int* data;
MyClass(int value) : data(new int(value)) {}
~MyClass() { delete data; }
// 拷贝构造函数
MyClass(const MyClass& other) : data(new int(*other.data)) {}
// 禁用移动构造函数
MyClass(MyClass&&) = delete;
};
int main() {
std::vector<MyClass> vec;
vec.emplace_back(1); // 调用构造函数
vec.emplace_back(2); // 可能扩容时调用拷贝构造函数
}
3. 没有拷贝或移动构造函数
如果既没有拷贝构造函数也没有移动构造函数,并且编译器无法生成隐式的构造函数,编译器会报错。
你的代码分析
对于你的 MyClass
:
class MyClass {
public:
int* data;
MyClass(int value) : data(new int(value)) {}
~MyClass() { delete data; }
// 注意:未显式提供拷贝构造或移动构造
};
- 如果没有显式定义拷贝和移动构造函数,编译器会隐式生成一个 拷贝构造函数 和一个 移动构造函数,扩容时优先调用隐式移动构造函数。
- 但是,由于没有显式处理
data
的动态内存管理(例如,深拷贝或移动语义),可能会导致 重复释放内存 的问题,从而引发崩溃。
结论
- 在
emplace_back(i)
时,不会调用拷贝构造或移动构造,而是直接调用匹配的构造函数。 - 在扩容时:
- 如果
MyClass
支持移动构造函数,调用移动构造函数。 - 如果不支持移动构造函数,但支持拷贝构造函数,调用拷贝构造函数。
- 如果两者都不支持,则报错。
- 如果
- 为了安全,建议显式定义拷贝构造和移动构造函数,并正确管理动态内存。