一.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.特点与关键点
-
自动生成的条件 (Rule of Five/The Rule of Zero)
-
编译器不会总是自动生成移动操作。
-
生成条件 :只有在用户没有显式定义拷贝操作 、移动操作 和析构函数中的任何一个时,编译器才会自动生成默认的移动构造函数和移动赋值运算符。
-
背后的逻辑 :如果你定义了析构函数或拷贝操作,通常意味着这个类需要管理某种资源,编译器无法确定默认的"逐个成员移动"行为是否正确和安全,因此它选择不生成,将选择权交给程序员。此时,移动操作会回退为拷贝操作(如果拷贝操作可用),这可能是低效的。
-
-
noexcept
说明符-
编译器生成的默认移动操作通常被标记为
noexcept
(不抛出异常)。 -
这非常重要,因为标准库容器(如
std::vector
)在重新分配内存时,如果元素的移动构造函数是noexcept
的,它会优先使用移动而不是拷贝来保证强异常安全。如果不是noexcept
,则会使用更保守的拷贝操作。
-
-
源对象状态
-
移动操作后,源对象(被移动的对象)的状态是"有效但未指定的"(valid but unspecified)。你不能再对它的值做任何假设,但可以对其进行析构或重新赋值。
-
对于像
std::string
或std::vector
这样的标准库类型,移动后它们通常处于空状态(.empty() == true
)。 -
对于内置类型,移动操作等同于拷贝,所以值保持不变。
-
-
与拷贝操作的区别
特性 拷贝操作 移动操作 目的 创建资源的独立副本 "窃取"资源,所有权转移 性能 开销大(深拷贝) 开销小(指针交换等) 源对象 操作后保持不变 操作后处于"有效但未指定"状态 参数 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;
}
主要用途:
-
统一类型 :可以将函数指针、lambda、仿函数(重载了
operator()
的类)、std::bind
表达式等所有可调用对象赋值给同一个std::function
类型,消除了它们的类型差异。 -
延迟调用:可以将可调用对象存储起来,在未来的某个时刻再调用它。这在实现回调函数、事件驱动系统、消息队列时非常有用。
-
作为函数参数 :函数可以使用
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();
}
}