C++11 类功能与包装器

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

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

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

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

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

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

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

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

cpp 复制代码
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; // 输出 42

    return 0;
}

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

行为:

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

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

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

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

  • 返回 *this 的引用。

cpp 复制代码
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::stringstd::vector 这样的标准库类型,移动后它们通常处于空状态(.empty() == true)。

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

  4. 与拷贝操作的区别

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

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

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

总结

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

2.default与delete

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

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

cpp 复制代码
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表达式等,但要注意参数类型要与可调用类型相匹配,例如:

cpp 复制代码
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指针,我们只需要将这个参数绑死就不需要再传入了。

cpp 复制代码
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.包装器与绑定的简单应用

逆波兰表达式

传统方法实现

cpp 复制代码
 //传统⽅式的实现
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实现

cpp 复制代码
// 使⽤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();
	}
}
相关推荐
FirstFrost --sy3 小时前
C++11 智能指针的使⽤及其原理
开发语言·c++·智能指针
万添裁3 小时前
C++的const_cast
开发语言·前端·javascript
River4163 小时前
Javer 学 c++(十二):与 JVM 对比篇
java·c++
程序喵大人4 小时前
手写智能指针:带你彻底搞懂 C++ 内存管理的底层逻辑
开发语言·c++·内存管理·智能指针
草莓熊Lotso4 小时前
【C++】详解形参和实参:别再傻傻分不清
c语言·开发语言·c++·经验分享·笔记·其他
百度森森4 小时前
【nuscenes数据集有关】
c++·数码相机·学习·ubuntu
吗喽对你问好4 小时前
Java场景题面试合集
java·开发语言·面试
一眼万里*e4 小时前
用ai写了个UE5插件
c++·ue5
He1955015 小时前
Go初级之九:Select 与并发控制
开发语言·后端·golang