【C++】移动语义与move()实用性教学
一、本质定义
移动语义的本质是资源的 “窃取”或 “接管”。
移动语义是C++11引入的资源管理机制,通过转移而非拷贝对象资源的所有权来提升性能。其核心在于右值引用(&&)和移动构造/赋值函数的组合实现。
移动语义是一种特殊的行为或者叫操作,std::move()函数是其中一个体现了移动语义的操作,并不是说移动语义一定是使用了std::move()函数。常见的实现了移动语义的操作还有"移动构造","移动赋值运算符",所有实现了移动语义的操作,肯定是将资源(数据或者文件)的管理权转移给其他变量管理,都不涉及释放资源和重新构造操作。
二、解决什么问题
(拷贝与移动两种方案的概念和代码对比分析)
传统拷贝方案:在传统的 C++ 中,当进行对象复制时,比如通过拷贝构造函数或拷贝赋值运算符,会创建目标对象的一份完整的副本,这涉及对资源(如动态内存、文件句柄等)的复制。
移动语义方案:移动语义允许我们将临时对象(通常是右值)所拥有的资源直接转移给目标对象,而不是进行复制。例如,在处理一个包含动态分配内存的类时,移动构造函数可以直接将临时对象的指针转移到新对象,而不用再分配内存并复制内容,这样大大减少了资源开销
以下是两种方案处理字符串赋值的操作对比,其中传统拷贝方案实现的效果是"给别人一份一样的";移动语义方案实现的效果是"拿走别人的那一份"。
#include <cstring>
#include <utility>
#include <iostream>class String {
private:char* data; // 只是声明,还没有分配内存空间public:// 基础构造函数explicit String(const char* str = "") {data = new char[strlen(str) + 1];strcpy_s(data, strlen(str) + 1, str);}// 传统拷贝构造函数String(const String& src) {data = new char[strlen(src.data) + 1];strcpy_s(data, strlen(src.data) + 1, src.data);std::cout << "Copied\n";}// 移动构造函数String(String&& src) noexcept {data = src.data;src.data = nullptr;std::cout << "Moved\n";}~String() { delete[] data; }
};void process(String s) { /* 使用副本 */ }int main() {String s1("Hello");// 1.传统拷贝方案String s2 = s1; // 输出"Copied"process(s1); // 输出"Copied"// 2.移动语义方案String s3 = std::move(s1); // 输出"Moved"process(std::move(s3)); // 输出"Moved"return 0;
}
三、什么功能
从代码实现的角度来看,移动语义只有一个操作,那就是将"资源管理权转移给别人"。移动语义的功能,作用,意义都是从此衍生出来的,包括:有效避免大型资源文件的深拷贝,避免资源的重复分配和释放;转移资源管理权,根据需求更换管理者。
四、怎么使用
(交代代码结构)
观察以下代码可以感受移动语义代码的触发时机, 代码编写逻辑,实际效果。其实下方代码结构很简单,主要是一个Resource类中,成员变量有两个,成员函数包含2个用于创建新对象的构造函数,2个用于重新赋值的'='重载运算符,移动语义就是这两个重载运算符中体现的。
#include <iostream>
#include <utility>class Resource {
private:int* data;std::string name;
public:// 1. 拷贝构造函数Resource(const Resource& other) : data(new int(*other.data)), name(other.name) {print("拷贝构造");}// 2. 移动构造函数Resource(Resource&& other) noexcept : data(other.data), name(std::move(other.name)) {other.data = nullptr;print("移动构造");}// 3. 拷贝赋值运算符Resource& operator=(const Resource& other) {if(this != &other) {delete data;data = new int(*other.data);name = other.name;}print("拷贝赋值");return *this;}// 4. 移动赋值运算符Resource& operator=(Resource&& other) noexcept {if(this != &other) {delete data;data = other.data;name = std::move(other.name);other.data = nullptr;}print("移动赋值");return *this;}// 辅助函数void print(const std::string& msg) const {std::cout << msg << " => data:" << (data ? *data : 0) << " name:" << name << std::endl;}// 基础构造/析构Resource(int val, std::string n) : data(new int(val)), name(n) {}~Resource() { delete data; }
};int main() {Resource r1(1, "原始资源");Resource r2 = r1; // 拷贝构造Resource r3(std::move(r1));// 移动构造(触发移动语义)Resource r4(2, "临时");r4 = r2; // 拷贝赋值r4 = std::move(r3); // 移动赋值(触发移动语义)return 0;
}
五、move()原理
(代码+解释)
std::move()是一个库函数,主要用于转移资源管理权,参数可以接收左值引用和右值引用类型,返回值一定是该参数变量的右值引用类型,原理是根据推导规则区分出参数变量的实际引用类型,一次决定返回值类型,使用类型强制转换操作符,将参数变量强制转换成右值引用类型,作为返回值。这样做的意义是将一个变量转换成右值引用。
template<typename T>
constexpr std::remove_reference_t<T>&& move(T&& t) noexcept {return static_cast<std::remove_reference_t<T>&&>(t);
}
①constexpr: 告诉编译器,被我修饰的函数或者表达式,可以在"编译时"求值。
②std::remove_reference_t<T> : 去除引用类型,将左值引用和右值引用都还原成基础类型。
③std::remove_reference_t<T> &&:组合效果,相当于(基础类型+&&)即变成右值引用类型。
④模板类函数的参数列表中的T&&:这是通用引用(也叫转发引用),可以同时接受左值引用和右值引用类型的变量,都可以作为参数,而且由于"类型推导规则"的存在,是能够区分出到底接受的是什么类型的引用聚。
⑤noexcept:告诉编译器,这个函数绝对不会抛出异常,不需要对这部分内容检查。
⑥static_cast<目标类型>(原始类型的变量):强制将原始类型的变量,转换为目标类型的变量,在此处的意义是,强行将类型为T的变量t,强制转换为,T&&右值引用类型的变量t。
作者结束语
(总结是每篇文章独立的,结语是我固定的)
这篇文章的结构安排应该是很清晰了,标题顺序,内容组织,代码解释都还不错,技术类文章的任务就是要教大家认识它,理解它,使用它。文章的内容我检查过很多次了,每篇文章都是一边查资料,一边理解整理,输出成文章,分享我的思考。点个免费的赞吧~
后续更多同样形式的分享,希望能通过我的文章给你更多思维的启发,更多思考问题的角度。