第一次在项目中遇到深浅拷贝问题是在三年前的一个多线程日志系统里。当时为了封装日志缓冲区的操作,我写了这样一个类:
cpp复制class LogBuffer {
public:
char* data;
size_t size;
LogBuffer(const char* str) {
size = strlen(str) + 1;
data = new char[size];
memcpy(data, str, size);
}
~LogBuffer() {
delete[] data;
}
};
看起来没什么问题对吧?直到我在另一个地方这样使用它:
cpp复制void processLog(LogBuffer buf) {
// 处理日志内容
}
int main() {
LogBuffer original("Hello World");
processLog(original); // 这里调用了默认的拷贝构造函数
// original.data 现在指向已释放的内存!
}
这就是典型的浅拷贝陷阱——默认的拷贝构造函数只是简单复制了指针值,导致两个对象指向同一块内存。当其中一个对象析构时,另一个对象的指针就变成了悬垂指针。
正确的做法是实现拷贝构造函数和拷贝赋值运算符:
cpp复制class LogBuffer {
public:
// ... 其他成员同上
LogBuffer(const LogBuffer& other) {
size = other.size;
data = new char[size];
memcpy(data, other.data, size);
}
LogBuffer& operator=(const LogBuffer& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new char[size];
memcpy(data, other.data, size);
}
return *this;
}
};
关键点:在拷贝赋值运算符中,一定要先检查自赋值情况(this == &other),否则在自赋值时会先删除自己的数据,导致后续拷贝出错。
假设我们要拷贝一个包含百万条记录的容器:
cpp复制std::vector<LogBuffer> hugeLogs;
auto copy = hugeLogs; // 这里会为每个元素调用拷贝构造函数
每个LogBuffer对象都需要:
对于大型对象或容器,这种拷贝开销可能成为性能瓶颈。
C++11引入了右值引用(&&)和移动语义,允许我们"偷取"临时对象的资源:
cpp复制class LogBuffer {
public:
// ... 其他成员同上
LogBuffer(LogBuffer&& other) noexcept {
data = other.data;
size = other.size;
other.data = nullptr; // 重要!防止原对象析构时释放资源
other.size = 0;
}
LogBuffer& operator=(LogBuffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
};
现在我们可以高效地转移资源所有权:
cpp复制LogBuffer createBuffer() {
LogBuffer temp("Temporary");
return temp; // 这里会调用移动构造函数
}
int main() {
LogBuffer buf = createBuffer(); // 没有深拷贝发生!
}
STL容器也全面支持移动语义:
cpp复制std::vector<LogBuffer> getHugeLogs() {
std::vector<LogBuffer> logs;
// 填充大量数据...
return logs; // 移动而非拷贝
}
cpp复制std::vector<int> src = {1, 2, 3};
std::vector<int> dest(src.size());
// 简单拷贝
std::copy(src.begin(), src.end(), dest.begin());
// 带转换的拷贝
std::transform(src.begin(), src.end(), dest.begin(),
[](int x) { return x * 2; });
C++11后许多算法支持移动语义:
cpp复制std::vector<std::string> strings = {"a", "bb", "ccc"};
std::vector<std::string> largeStrings;
// 移动满足条件的元素
std::copy_if(std::make_move_iterator(strings.begin()),
std::make_move_iterator(strings.end()),
std::back_inserter(largeStrings),
[](const std::string& s) { return s.size() > 1; });
// strings中的"bb"和"ccc"现在处于有效但未指定状态
排序算法通常需要交换元素,C++11后使用移动语义优化:
cpp复制std::vector<ExpensiveObject> objs;
std::sort(objs.begin(), objs.end()); // 内部使用移动而非拷贝交换元素
不是所有类都需要移动语义。适合实现的场景:
移动后的源对象应处于:
移动操作应标记为noexcept,否则某些STL操作会回退到拷贝:
cpp复制// 没有noexcept可能导致vector扩容时使用拷贝而非移动
LogBuffer(LogBuffer&& other) noexcept;
现代编译器会自动优化:
cpp复制LogBuffer create() {
return LogBuffer("Hello"); // 可能直接在调用处构造,不调用任何拷贝/移动
}
如果一个类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符,那么它很可能也需要移动构造函数和移动赋值运算符。
cpp复制class ResourceHolder {
public:
ResourceHolder() = default;
~ResourceHolder() = default;
// 禁止拷贝
ResourceHolder(const ResourceHolder&) = delete;
ResourceHolder& operator=(const ResourceHolder&) = delete;
// 允许移动
ResourceHolder(ResourceHolder&&) = default;
ResourceHolder& operator=(ResourceHolder&&) = default;
};
cpp复制class SafeBuffer {
std::unique_ptr<char[]> data;
size_t size;
public:
// 不需要手动实现拷贝/移动/析构
// 编译器生成的默认行为就是正确的
};
让我们用实际数据看看差异:
cpp复制struct HeavyData {
std::array<int, 1000> data;
HeavyData() { std::iota(data.begin(), data.end(), 0); }
// 实现拷贝和移动语义
};
void testCopy() {
std::vector<HeavyData> vec(1000);
auto copy = vec; // 拷贝
}
void testMove() {
std::vector<HeavyData> vec(1000);
auto moved = std::move(vec); // 移动
}
测试结果(i7-11800H @2.30GHz):
cpp复制std::vector<HeavyData> getData() {
std::vector<HeavyData> data;
// ...
return data; // 正确的移动
}
auto data = getData(); // 好的
void process(const std::vector<HeavyData>& data);
process(getData()); // 可能产生临时对象的拷贝
解决方案:确保process有移动语义版本:
cpp复制void process(std::vector<HeavyData>&& data);
cpp复制std::string str = "Hello";
std::string stolen = std::move(str);
std::cout << str.length(); // 未定义行为!
cpp复制std::ostream& operator<<(std::ostream& os, const LogBuffer& buf) {
os << "LogBuffer[size=" << buf.size << ", data=" << (void*)buf.data << "]";
return os;
}
模板代码需要特别考虑通用引用和完美转发:
cpp复制template<typename T>
void wrapper(T&& arg) {
// 根据arg是左值还是右值选择拷贝或移动
process(std::forward<T>(arg));
}
STL中的emplace_back就是典型应用:
cpp复制std::vector<ComplexObj> objs;
objs.emplace_back(arg1, arg2); // 直接在容器内构造,避免拷贝/移动
移动操作通常应为noexcept,但有时需要权衡:
cpp复制class SafeMove {
std::vector<int> data;
public:
SafeMove(SafeMove&& other) noexcept(noexcept(std::vector<int>(std::move(other.data))))
: data(std::move(other.data)) {}
};
移动操作通常比拷贝更适用于多线程场景,因为:
cpp复制std::unique_ptr<Data> globalData;
void updateData() {
auto newData = std::make_unique<Data>(...);
std::lock_guard<std::mutex> lock(mtx);
globalData = std::move(newData); // 原子指针交换
}
在最近的一个高性能网络服务器项目中,我们通过全面应用移动语义,将消息处理吞吐量提升了40%。关键是将所有大型消息对象的传递改为移动语义,避免了不必要的拷贝。特别是在处理10KB以上的数据包时,移动语义带来的性能提升更为明显。