云计算百科
云计算领域专业知识百科平台

C++移动构造函数详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

要弄明白移动构造函数是什么,需要首先了解什么是左值右值及右值引用,如果不了解,可以先看这篇文章:c++左值右值和右值引用

什么是移动构造函数

移动构造函数是C++11引入的一种特殊构造函数,用于从临时对象(右值)"窃取"资源来初始化新对象,而不是进行昂贵的深拷贝。

class MyString {
char* data;
size_t size;
public:
// 移动构造函数:参数是右值引用 &&
MyString(MyString&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // 关键:把源对象的资源置空
other.size = 0;
}
};

二、为什么需要它

核心问题:深拷贝昂贵

考虑 std::vector 内部持有一块堆内存:

std::vector<int> v1 = {1,2,3,…,1000000};
std::vector<int> v2 = std::move(v1); // 期望:转移指针,O(1)

如果用拷贝构造:需要重新分配100万个int并逐个复制 → O(n),慢。

如果用移动构造:只需把内部指针交给新对象,把源对象指针置空 → O(1),快。

典型触发场景:

  • 函数返回局部对象(返回值优化配合)

  • 容器扩容时搬迁元素(vector 重新分配)

  • 显式 std::move 转移所有权

  • 传递临时对象

std::vector<std::string> getStrings() {
std::vector<std::string> v;
// … 填充大量字符串
return v; // 移动而非拷贝(RVO/NRVO)
}

熟悉c++的话,无论是函数传参还是赋值等等,c++很容易将变量拷贝一个副本去操作副本,但是如果你在类中声明了移动构造函数,编译器就会在满足条件的时候如这里的典型触发场景不拷贝而是移动,这样从性能上来讲是有优化的

三、如何正确使用

1. 标准写法(含 noexcept)

MyString.h

#include <cstddef>
#include <cstring>
#include <iostream>
#include <utility>
#include <windows.h> // 必须包含这个头文件,使vscode支持中文输出

class MyString {
public:
explicit MyString(const char* text = "") {
size = text == nullptr ? 0 : std::strlen(text);
data = new char[size + 1];
if (size == 0) {
data[0] = '\\0';
} else {
std::memcpy(data, text, size + 1);
}

std::cout << "[构造] 内容=\\"" << data
<< "\\", 资源地址=" << static_cast<const void*>(data) << '\\n';
}

MyString(const MyString& other)
: data(new char[other.size + 1]), size(other.size) {
if (other.data == nullptr) {
data[0] = '\\0';
} else {
std::memcpy(data, other.data, size + 1);
}

std::cout << "[拷贝构造] 深拷贝到资源地址="
<< static_cast<const void*>(data) << '\\n';
}

MyString(MyString&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;

std::cout << "[移动构造] 接管资源地址="
<< static_cast<const void*>(data)
<< ",并将原对象置为空\\n";
}

MyString& operator=(const MyString& other) {
if (this == &other) {
return *this;
}

char* newData = new char[other.size + 1];
if (other.data == nullptr) {
newData[0] = '\\0';
} else {
std::memcpy(newData, other.data, other.size + 1);
}

delete[] data;
data = newData;
size = other.size;

std::cout << "[拷贝赋值] 释放旧资源并深拷贝到资源地址="
<< static_cast<const void*>(data) << '\\n';
return *this;
}

MyString& operator=(MyString&& other) noexcept {
if (this == &other) {
return *this;
}

char* oldData = data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
delete[] oldData;

std::cout << "[移动赋值] 释放旧资源并接管资源地址="
<< static_cast<const void*>(data)
<< ",并将原对象置为空\\n";
return *this;
}

~MyString() {
if (data != nullptr) {
std::cout << "[析构] 释放资源地址="
<< static_cast<const void*>(data)
<< ",内容=\\"" << data << "\\"\\n";
} else {
std::cout << "[析构] 空对象无需释放资源\\n";
}
delete[] data;
}

const char* c_str() const {
return data == nullptr ? "" : data;
}

std::size_t length() const {
return size;
}

void print(const char* label) const {
std::cout << label << ": 内容=\\"" << c_str()
<< "\\", 长度=" << size
<< ", 资源地址=" << static_cast<const void*>(data) << '\\n';
}

private:
char* data;
std::size_t size;
};

TestMove.cpp

#include "MyString.h"

int main() {

// 设置控制台输出编码为 UTF-8
SetConsoleOutputCP(65001);

MyString source("Hello, move semantics!");
source.print("source");

std::cout << "\\n===== 1. 移动构造:交出资源所有权 =====" << '\\n';
MyString moved(std::move(source));
source.print("source(移动后)");
moved.print("moved");

std::cout << "\\n===== 2. 移动赋值:接管另一个对象的资源 =====" << '\\n';
MyString target("old data");
target.print("target(移动前)");
target = std::move(moved);
moved.print("moved(移动后)");
target.print("target(移动后)");

std::cout << "\\n===== 3. 被移动的对象可以重新赋值 =====" << '\\n';
moved = MyString("reused");
moved.print("moved(重新赋值后)");

std::cout << "\\n===== 4. main 结束,对象按相反顺序析构 =====" << '\\n';
return 0;
}

可以将代码拷贝到IDE运行一下看看运行结果,运行结果文字较多就不贴出来了以免影响下面的阅读

2. 关键要点
要点说明
参数类型 T&&(右值引用),不是 const T&&
标记 noexcept std::vector 扩容时只有在移动构造为 noexcept 时才会用移动,否则退化为拷贝
源对象置为有效状态 必须让 other 处于可安全析构的状态(通常置空指针/清空)
自赋值检查 移动赋值中要检查 this != &other
五法则 定义了析构/拷贝/移动之一,通常需考虑全部五个

这里提一下其中的两点

让我们回到上述代码中的其中一个片段

MyString(MyString&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;

std::cout << "[移动构造] 接管资源地址="
<< static_cast<const void*>(data)
<< ",并将原对象置为空\\n";
}

other.data = nullptr; other.size = 0;

为什么要把原对象置空?因为原对象是个右值,马上就要被销毁了。但它的析构函数还是会调用的。如果不置空,析构函数就会把 data_ 给 delete 掉——那新对象就成野指针了。

析构函数这么写

~MyString() {
if (data != nullptr) {
std::cout << "[析构] 释放资源地址="
<< static_cast<const void*>(data)
<< ",内容=\\"" << data << "\\"\\n";
} else {
std::cout << "[析构] 空对象无需释放资源\\n";
}
delete[] data;
}

什么是五法则

Rule of Five(五法则,C++11 及以后) C++11 引入了移动语义,新增了两个特殊成员函数。五法则建议,如果你需要定义(或 =delete)以下五个中的任意一个,就应该考虑全部五个:

析构函数

拷贝构造函数

拷贝赋值运算符

移动构造函数 (C++11)

移动赋值运算符 (C++11) 这也是为什么上面代码中的MyString 写了那么多的原因,MyString.h就是遵循了五法则

3. 用 std::move 强制转移

Buffer b1(1024);
Buffer b2 = std::move(b1); // b1 之后不应再使用其资源(但可析构/重新赋值)

std::string s1 = "hello";
std::string s2 = std::move(s1);
// s1 现在处于"有效但未指定"状态,通常为空

4. 常见陷阱

// ❌ 错误:移动构造没置空源对象 → 双重释放
Buffer(Buffer&& o) : ptr(o.ptr) {} // o.ptr 未置空,析构时 delete 两次

// ❌ 错误:参数写成 const T&& → 无法修改源对象
Buffer(const Buffer&& o);

// ❌ 错误:移动构造未标 noexcept → vector 扩容走拷贝
Buffer(Buffer&& o);

// ❌ 对 const 对象 std::move 无效 → 退化为拷贝
const std::string cs = "x";
std::string s = std::move(cs); // 实际调用拷贝构造

5. 何时该写/不该写
  • 需要手写:类自己管理资源(裸指针、文件句柄、socket 等)

  • 不需要手写:成员都是 std::string/std::vector/智能指针等,编译器生成的默认移动构造已足够

  • = default 显式生成:

class Widget {
std::string name;
std::vector<int> data;
public:
Widget(Widget&&) = default;
Widget& operator=(Widget&&) = default;
// 拷贝也 default 即可
};

赞(0)
未经允许不得转载:网硕互联帮助中心 » C++移动构造函数详解
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!