当前位置: 首页 > news >正文

C++11 类功能与包装器

一.C++11中新的类功能

1.默认的移动构造和移动赋值

1. 默认移动构造函数 (T(T&&))

行为:当一个类没有显式定义移动构造函数时,编译器在满足特定条件(见后文)时会自动生成一个默认的移动构造函数。

它的行为是:对其每个成员(包括基类成员)逐个进行移动初始化。

  • 对于内置类型(如 intdouble, 原始指针等):执行逐位拷贝(bitwise copy)。因为它们是“值”,没有“资源”可移动,所以拷贝和移动没有区别。

  • 对于类类型成员:调用该成员的移动构造函数

  • 对于数组成员:对数组中的每个元素执行上述规则。

class MyClass {
public:std::string str; // 类类型成员int num;         // 内置类型成员// 编译器为我们生成默认的移动构造函数,类似于:// MyClass(MyClass&& other) noexcept//     : str(std::move(other.str)), // 调用 string 的移动构造函数//       num(other.num)            // 内置类型,直接拷贝// {}
};int main() {MyClass obj1;obj1.str = "Hello";obj1.num = 42;MyClass obj2 = std::move(obj1); // 调用默认移动构造函数std::cout << obj2.str << std::endl; // 输出 "Hello"std::cout << obj2.num << std::endl; // 输出 42// obj1 的 str 已被“掏空”,处于有效但未指定的状态(通常是空字符串)std::cout << obj1.str.size() << std::endl; // 很可能输出 0// obj1.num 的值不变,因为它只是被拷贝了std::cout << obj1.num << std::endl; // 输出 42return 0;
}

2. 默认移动赋值运算符 (T& operator=(T&&))

行为:

同样地,当没有显式定义时,编译器在满足条件时会生成默认的移动赋值运算符。

它的行为是:对其每个成员(包括基类成员)逐个进行移动赋值。

  • 释放当前对象(*this)拥有的资源(通过调用各成员的析构函数或赋值运算符)。

  • 从源对象(右值引用)中“窃取”资源。

  • 返回 *this 的引用。

class MyClass {
public:std::string str;int num;// 编译器为我们生成默认的移动赋值运算符,类似于:// MyClass& operator=(MyClass&& other) noexcept {//     str = std::move(other.str); // 调用 string 的移动赋值运算符//     num = other.num;           // 内置类型,直接赋值//     return *this;// }
};int main() {MyClass obj1;obj1.str = "Hello";obj1.num = 42;MyClass obj2;obj2 = std::move(obj1); // 调用默认移动赋值运算符// 效果与移动构造函数类似:obj2 获得资源,obj1 被“掏空”return 0;
}

3.特点与关键点

  1. 自动生成的条件 (Rule of Five/The Rule of Zero)

    • 编译器不会总是自动生成移动操作。

    • 生成条件:只有在用户没有显式定义拷贝操作移动操作析构函数中的任何一个时,编译器才会自动生成默认的移动构造函数和移动赋值运算符。

    • 背后的逻辑:如果你定义了析构函数或拷贝操作,通常意味着这个类需要管理某种资源,编译器无法确定默认的“逐个成员移动”行为是否正确和安全,因此它选择不生成,将选择权交给程序员。此时,移动操作会回退为拷贝操作(如果拷贝操作可用),这可能是低效的。

  2. noexcept 说明符

    • 编译器生成的默认移动操作通常被标记为 noexcept(不抛出异常)。

    • 这非常重要,因为标准库容器(如 std::vector)在重新分配内存时,如果元素的移动构造函数是 noexcept 的,它会优先使用移动而不是拷贝来保证强异常安全。如果不是 noexcept,则会使用更保守的拷贝操作。

  3. 源对象状态

    • 移动操作后,源对象(被移动的对象)的状态是“有效但未指定的”(valid but unspecified)。你不能再对它的值做任何假设,但可以对其进行析构或重新赋值。

    • 对于像 std::string 或 std::vector 这样的标准库类型,移动后它们通常处于空状态(.empty() == true)。

    • 对于内置类型,移动操作等同于拷贝,所以值保持不变。

  4. 与拷贝操作的区别

    特性拷贝操作移动操作
    目的创建资源的独立副本“窃取”资源,所有权转移
    性能开销大(深拷贝)开销小(指针交换等)
    源对象操作后保持不变操作后处于“有效但未指定”状态
    参数const T& (常量左值引用)T&& (右值引用)

当你的类管理着动态资源(如原始指针、文件句柄等),而默认的“逐个成员移动”行为不正确时(例如,默认移动一个原始指针只是拷贝了指针值,会导致双重释放),你需要自己定义移动操作来实现资源的正确转移,并将源对象的资源指针置为 nullptr

现代 C++ 的最佳实践是使用 RAII 原则,用智能指针(std::unique_ptrstd::shared_ptr)和标准库容器来管理资源。这些类已经完美实现了移动语义,因此你通常不需要自己定义析构函数、拷贝/移动操作(遵循 The Rule of Zero),编译器生成的默认行为就是正确且高效的。

总结

特性默认移动构造函数默认移动赋值运算符
行为对每个成员进行移动初始化对每个成员进行移动赋值
生成条件用户未定义五巨头(拷贝构造、拷贝赋值、移动构造、移动赋值、析构)中的任何一个同上
异常规范noexceptnoexcept
优点高效,避免不必要的拷贝同上
缺点对管理原始资源的类不安全(浅拷贝问题)同上
最佳实践使用 RAII 对象管理资源,依赖编译器生成的默认操作(Rule of Zero)同上

2.default与delete

• C++11可以让你更好的控制要使⽤的默认函数。假设你要使⽤某个默认的函数,但是因为⼀些原因这个函数没有默认⽣成。⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤default关键字显⽰指定移动构造⽣成。

• 如果能想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明补丁,这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指⽰编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。

class Person
{
public:Person(const char* name = "张三", int age = 1):_name(name), _age(age){}Person(const Person& p) = default;Person(Person&& p) = default;~Person(){}
private:wjh::string _name;int _age;
};void func(ostream& out)
{}

二.包装器与绑定

1.包装器

包装器function是一种类模板,它主要用于接收各种可调用的类型,例如函数,类模板,lambda表达式等,但要注意参数类型要与可调用类型相匹配,例如:

class Plus
{
public:Plus(int n = 10):_n(n){}static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return (a + b) * _n;}private:int _n;
};int main()
{// 包装各种可调用对象function<int(int, int)> f1 = f;function<int(int, int)> f2 = Functor();function<int(int, int)> f3 = [](int a, int b) {return a + b; };cout << f1(1, 1) << endl;cout << f2(1, 1) << endl;cout << f3(1, 1) << endl;// 包装静态成员函数// 成员函数要指定类域并且前面加&才能获取地址function<int(int, int)> f4 = &Plus::plusi;cout << f4(1, 1) << endl;function<double(Plus*, double, double)> f5 = &Plus::plusd;Plus pl;cout << f5(&pl, 1.111, 1.1) << endl;function<double(Plus, double, double)> f6 = &Plus::plusd;cout << f6(pl, 1.1, 1.1) << endl;cout << f6(Plus(), 1.1, 1.1) << endl;function<double(Plus&&, double, double)> f7 = &Plus::plusd;cout << f7(move(pl), 1.1, 1.1) << endl;cout << f7(Plus(), 1.1, 1.1) << endl;map<string, function<int(int, int)>> opFuncMap = {{"+", [](int x, int y) {return x + y; }},{"-", [](int x, int y) {return x - y; }},{"*", [](int x, int y) {return x * y; }},{"/", [](int x, int y) {return x / y; }},{"&", [](int x, int y) {return x + y; }},{"|", [](int x, int y) {return x | y; }},{"^", [](int x, int y) {return x ^ y; }}};return 0;
}

主要用途:

  1. 统一类型:可以将函数指针、lambda、仿函数(重载了 operator() 的类)、std::bind 表达式等所有可调用对象赋值给同一个 std::function 类型,消除了它们的类型差异。

  2. 延迟调用:可以将可调用对象存储起来,在未来的某个时刻再调用它。这在实现回调函数、事件驱动系统、消息队列时非常有用。

  3. 作为函数参数:函数可以使用 std::function 作为参数来接收外部传入的可调用策略,使函数接口更加通用和灵活,而无需使用函数指针或模板(模板会导致代码膨胀)。

2.绑定

std::bind 是一个函数模板,它像一个通用的函数适配器。它接受一个可调用对象,并生成一个新的可调用对象,这个新对象可以“绑定”原对象的部分参数,或者重新排列参数的顺序。这里最直接的应用就是调整参数顺序或者绑定死某个参数。例如上面对类成员函数的包装,第一个参数需要是this指针,我们只需要将这个参数绑死就不需要再传入了。

auto sub1 = bind(Sub, _1, _2);
cout << sub1(10, 5) << endl;auto sub2 = bind(Sub, _2, _1);
cout << sub2(10, 5) << endl;// 调整参数个数 (常用)
auto sub3 = bind(Sub, 100, _1);
cout << sub3(5) << endl;auto sub4 = bind(Sub, _1, 100);
cout << sub4(5) << endl;// 分别绑死第123个参数
auto sub5 = bind(SubX, 100, _1, _2);
cout << sub5(5, 1) << endl;
auto sub6 = bind(SubX, _1, 100, _2);
cout << sub6(5, 1) << endl;
auto sub7 = bind(SubX, _1, _2, 100);
cout << sub7(5, 1) << endl;// 成员函数对象进行绑死,就不需要每次都传递了
*function<double(Plus&&, double, double)> f6 = &Plus::plusd;
Plus pd;
cout << f6(move(pd), 1.1, 1.1) << endl;
cout << f6(Plus(), 1.1, 1.1) << endl;*/function<double(double, double)> f6 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f6(1.1, 1.1) << endl;

3.包装器与绑定的简单应用

逆波兰表达式

传统方法实现

 //传统⽅式的实现
class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> st;for (auto& str : tokens){if(str == "+" || str == "-" || str == "*" || str == "/"){int right = st.top();st.pop();int left = st.top();st.pop();switch (str[0]){case '+':st.push(left + right);break;case '-':st.push(left - right);break;case '*':st.push(left * right);break;case '/':st.push(left / right);break;}} else{st.push(stoi(str));}} return st.top();}
};

使用映射+function实现

// 使⽤map映射string和function的⽅式实现
// 这种⽅式的最⼤优势之⼀是⽅便扩展,假设还有其他运算,我们增加map中的映射即可
class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> st;// function作为map的映射可调⽤对象的类型map<string, function<int(int, int)>> opFuncMap = {{"+", [](int x, int y) {return x + y; }},{"-", [](int x, int y) {return x - y; }},{"*", [](int x, int y) {return x * y; }},{"/", [](int x, int y) {return x / y; }}};for(auto & str : tokens){if (opFuncMap.count(str)) // 操作符{int right = st.top();st.pop();int left = st.top();st.pop();int ret = opFuncMap[str](left, right);st.push(ret);} else{st.push(stoi(str));}} return st.top();}
}

http://www.xdnf.cn/news/1453159.html

相关文章:

  • 基于Taro4打造的一款最新版微信小程序、H5的多端开发简单模板
  • 盲盒抽卡机小程序系统开发:以技术创新驱动娱乐体验升级
  • 算法训练营DAY58 第十一章:图论part08
  • elasticsearch-7.17.29 集群案例
  • helm 的常用命令
  • Spring Cloud Eureka 核心原理
  • React 中的 HOC 和 Hooks
  • Java Web 是技术与产业的 “交叉赋能点”
  • 原生住宅IP有多顶?跨境圈都在用
  • MaxKB4j智能体平台 Docker Compose 快速部署教程
  • webrtc之语音活动上——VAD能量检测原理以及源码详解
  • 桌面应用开发语言与框架选择指南
  • android seekbar显示刻度
  • Python_occ 学习记录 | 细观建模(2)
  • 【C语言】第二课 位运算
  • QT6 配置 Copilot插件
  • Pycharm 试用
  • Spring简单的读取和存储对象
  • 君正T31学习(7)- 启动流程
  • 当有鹿机器人读懂城市呼吸的韵律——具身智能如何重构户外清洁生态
  • 2025变现打法:AI+IP实现高效变现|创客匠人
  • 第十四届蓝桥杯青少组C++国赛[2023.5.28]第二部分编程题(4、 数独填数)
  • JS中正则表达式的运用
  • android Thread线程—HandlerThread
  • 汽车v型推力杆总成三维5自由度性能及疲劳测试系统
  • 追觅科技举办2025「敢梦敢为」发布会,发布超30款全场景重磅新品
  • 【iOS】 懒加载
  • 每日工作计划管理工具:核心功能详解
  • 《Java餐厅的待客之道:BIO, NIO, AIO三种服务模式的进化》
  • UE5 制作游戏框架的部分经验积累(持续更新)