C++11相关知识点
一.左值引用和右值引用
1.1左值概念和右值概念
左值:左值是一个表示数据的表达式(如变量名或解引用的指针),一般情况下,我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。
右值:右值也是一个表示数据的表达式,如:字面常量、表达式返回值,传值返回函数的返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。
常见的左值和右值:
//左值:可以取地址 // 以下的p、b、c、*p、s、s[0]就是常⻅的左值 int* p = new int(0);int b = 1;const int c = b;*p = 10;string s("111111");s[0] = 'x';cout << &c << endl;cout << (void*)&s[0] << endl;// 右值:不能取地址 double x = 1.1, y = 2.2;// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值 10;x + y;fmin(x, y);string("11111");
1.2左值引用和右值引用
对于左值引用而言,就是给左值取别名,同理右值引用就是给右值取别名。
左值引用:Type& r1 = x;
右值引用:Type&& rr1 = y;
左值引用不能直接引用右值,但是const左值引用可以引用右值, 右值引用不能直接引用左值,但是右值引用可以引用move(左值)。
//左值引用引用右值
int a = 10;
int& ra1 = a;
const int& ra2 = a;
//右值引用引用左值
int&& r1 = 20;
int&& r2 = move(a);
return 0;
1.2.1引用用于延长生命周期
右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象生存期,但这些对象无法被修改
std::string s1 = "Test";// std::string&& r1 = s1; // 错误:不能绑定到左值 const std::string& r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期 // r2 += "Test"; // 错误:不能通过到 const 的引⽤修改 std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期
r3 += "Test"; // OK:能通过到⾮ const 的引⽤修改
1.2.2左值引用相关回顾
左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如下面addStrings函数。是因为传值返回的时候需要进行一次深拷贝,那么在vector等数据结构中传值返回将会变得十分麻烦。
// 传值返回需要拷⻉ string addStrings(string num1, string num2) {string str;int end1 = num1.size()-1, end2 = num2.size()-1;// 进位 int next = 0;while(end1 >= 0 || end2 >= 0){int val1 = end1 >= 0 ? num1[end1--]-'0' : 0;int val2 = end2 >= 0 ? num2[end2--]-'0' : 0;int ret = val1 + val2+next;next = ret / 10;ret = ret % 10;str += ('0'+ret);}if(next == 1)str += '1';reverse(str.begin(), str.end());return str;
}
1.2.3移动构造和移动赋值
移动构造函数是⼀种构造函数,类似拷贝构造函数,移动构造函数要求第⼀个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
移动赋值是⼀个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第⼀个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引用的类型,他的本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提⾼效率。
1.2.4右值引用和移动语义解决传值返回问题
右值返回实际上就是使用了移动构造和移动赋值,其核心就是:
移动构造函数:在用一个临时对象初始化新对象时,直接 "窃取" 临时对象所持有的资源,而不是复制资源。临时对象在交出资源后会被销毁,避免了不必要的内存分配和复制操作。
移动赋值运算符:类似于移动构造,当用一个临时对象给已有对象赋值时,将临时对象的资源转移到目标对象,同时释放目标对象原有的资源,同样避免了复制开销。
核心:转移资源而不是重新开空间复制
1.2.5引用折叠
规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
- 左值引用的左值引用折叠为左值引用(T& & → T&)
- 左值引用的右值引用折叠为左值引用(T& && → T&)
- 右值引用的左值引用折叠为左值引用(T&& & → T&)
- 右值引用的右值引用折叠为右值引用(T&& && → T&&)
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{}
int main()
{typedef int& lref;typedef int&& rref;int n = 0;lref& r1 = n; // r1 的类型是 int& lref&& r2 = n; // r2 的类型是 int& rref& r3 = n; // r3 的类型是 int& rref&& r4 = 1; // r4 的类型是 int&& // 没有折叠->实例化为void f1(int& x) f1<int>(n);f1<int>(0); // 报错 // 折叠->实例化为void f1(int& x) f1<int&>(n);f1<int&>(0); // 报错 // 折叠->实例化为void f1(int& x) f1<int&&>(n);//右值引用左值仍然为左值引用f1<int&&>(0); // 报错 // 折叠->实例化为void f1(const int& x) f1<const int&>(n);f1<const int&>(0);// 折叠->实例化为void f1(const int& x) f1<const int&&>(n);
f1<const int&&>(0);// 没有折叠->实例化为void f2(int&& x) f2<int>(n); // 报错 f2<int>(0);// 折叠->实例化为void f2(int& x) f2<int&>(n);f2<int&>(0); // 报错 // 折叠->实例化为void f2(int&& x) f2<int&&>(n); // 报错 f2<int&&>(0);return 0;
}template<class T>
void Function(T&& t)
{int a = 0;T x = a;//x++;cout << &a << endl;cout << &x << endl << endl;
}
int main()
{// 10是右值,推导出T为int,模板实例化为void Function(int&& t) Function(10); // 右值 int a;// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t) Function(a); // 左值 // std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t) Function(std::move(a)); // 右值 const int b = 8;// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&
t)// 所以Function内部会编译报错,x不能++ Function(b);
// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&
t)// 所以Function内部会编译报错,x不能++ Function(std::move(b)); // const 右值 return 0;
}
1.2.6完美转发
完美转发(Perfect Forwarding) 是 C++ 中用于在模板函数中保持参数原始属性(左值 / 右值特性)的技术,核心是通过引用折叠和 std::forward 实现参数的 "原样转发"。
当我们需要将一个函数的参数传递给另一个函数时,完美转发能确保:
- 若原始参数是左值,转发后仍为左值
- 若原始参数是右值,转发后仍为右值
不使用完美转发:
#include <iostream>
#include <utility>// 目标函数:分别处理左值和右值
void process(int& x) {std::cout << "处理左值: " << x << "\n";
}
void process(int&& x) {std::cout << "处理右值: " << x << "\n";
}// 包装函数(不完美转发)
template<typename T>
void wrap(T param) { // 参数会被拷贝,丢失原始属性process(param); // 始终作为左值传递
}int main() {int a = 10;wrap(a); // 传入左值,但wrap中param是左值wrap(20); // 传入右值,但wrap中param是左值(拷贝后的变量)return 0;
}
//输出
//处理左值: 10
//处理左值: 20 // 问题:右值被当作左值处理了
使用完美转发:
#include <iostream>
#include <utility>// 目标函数(同上)
void process(int& x) {std::cout << "处理左值: " << x << "\n";
}
void process(int&& x) {std::cout << "处理右值: " << x << "\n";
}// 完美转发的包装函数
template<typename T>
void perfect_wrap(T&& param) { // 通用引用(依赖引用折叠)process(std::forward<T>(param)); // 保持原始值属性转发
}int main() {int a = 10;perfect_wrap(a); // 传入左值 → 转发为左值perfect_wrap(20); // 传入右值 → 转发为右值perfect_wrap(std::move(a)); // 强制转为右值 → 转发为右值return 0;
}
//输出
//处理左值: 10
//处理右值: 20
//处理右值: 10 // 正确保持了右值特性
二.lambda表达式
lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。 lambda 表达式语法使用层而言没有类型,所以我们⼀般是用auto或者模板参数定义的对象去接收 lambda 对象。
lambda表达式的格式:
[capture-list] (parameters)-> return type { function boby }
• [capture-list] :捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来 判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉,捕捉列表为空也不能省略。
• (parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连 同()⼀起省略
• ->return type :返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此 部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
• {function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以 使⽤其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
auto ret = [](int x, int y) {return x + y; };
cout << ret(3, 4) << endl;
2.1捕捉列表
lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉
2.1.1 传值捕捉和传引用捕捉
// 局部的静态和全局变量不能捕捉,也不需要捕捉 static int m = 0;auto func6 = []{int ret = x + m;return ret;};
int a = 1, b = 2, c = 3, d = 4;
auto ret1 = [a, &b](){//传值的值不能被改变,传引用可以被改变a++;//报错b++;return a + b;};
2.1.2在捕捉列表中隐式捕捉
在捕捉列表写⼀个=表示隐式值捕捉,在捕捉列表 写⼀个&表示隐式引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量。
//隐式捕捉
auto ret2 = [&](){a++;b++;c++;return a + b + c;};
auto func2 = [=]{int ret = a + b + c;return ret;};cout << func2() << endl;
2.1.3在捕捉列表中混合使用隐式捕捉和显示捕捉
//混合捕捉
auto ret3 = [&, a, b](){c++;d++;return a + b + c + d;};
默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改, mutable加在参数列表的后⾯可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以 修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为 空)。
// 传值捕捉本质是⼀种拷⻉,并且被const修饰了 // mutable相当于去掉const属性,可以修改了 // 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷⻉ auto func7 = [=]()mutable{a++;b++;c++;d++;return a + b + c + d;};
三.包装器function
std::function 是一个通用的函数封装器(位于 <functional> 头文件中),它可以存储、复制和调用任何可调用对象(函数、 lambda 表达式、函数指针、仿函数、绑定表达式等),是实现回调机制、函数作为参数传递的重要工具。
格式:
std::function<返回值类型(参数类型1, 参数类型2, ...)>
3.1存储不同类型的可调用对象
#include <iostream>
#include <functional>// 普通函数
int add(int a, int b) {return a + b;
}// 仿函数(函数对象)
struct Multiply {int operator()(int a, int b) {return a * b;}
};int main() {// 存储普通函数std::function<int(int, int)> func1 = add;std::cout << "10 + 20 = " << func1(10, 20) << "\n"; // 输出 30// 存储仿函数std::function<int(int, int)> func2 = Multiply();std::cout << "10 * 20 = " << func2(10, 20) << "\n"; // 输出 200// 存储 lambda 表达式std::function<int(int, int)> func3 = [](int a, int b) {return a - b;};std::cout << "10 - 20 = " << func3(10, 20) << "\n"; // 输出 -10return 0;
}
3.2作为函数参数传递(回调函数)
#include <functional>
class ppc
{
public:static int add1(int x, int y){return x + y;}int mul1(int x,int y){return x * y;}
private:int n;
};
void test1()
{//包装静态成员函数 // 成员函数要指定类域并且前⾯加&才能获取地址 function<int(int, int)> f1 = &ppc::add1;// 包装普通成员函数// 普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以 ppc ppc1;function<int(ppc, int, int)> f2 = &ppc::mul1;cout << f2(ppc1, 1, 2) << endl;function<int(ppc*, int, int)> f3 = &ppc::mul1;cout << f3(&ppc1, 2, 3) << endl;function<int(ppc&, int, int)> f4 = &ppc::mul1;cout << f4(ppc1, 2, 3) << endl;function<int(ppc&&, int, int)> f5 = &ppc::mul1;cout << f5(move(ppc1), 2, 3) << endl;
}
四.bind
std::bind 是 C++ 标准库(位于 <functional> 头文件)中的一个函数模板,用于将函数与部分或全部参数绑定,生成一个新的可调用对象。它的核心作用是调整函数参数的数量和顺序,实现参数的预绑定或重排。
4.1调整参数顺序
#include<functional>
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int sub1(int x, int y)
{return x - y;
}
int add2(int x, int y,int z)
{return x + y+z;
}
void test2()
{auto bind1 = bind(sub1, _1, _2);cout << bind1(10, 5) << endl;auto bind2 = bind(sub1, _2, _1);cout << bind2(5, 10) << endl;
}
int main()
{test2();return 0;
}
4.2绑定普通函数(固定部分参数)
auto bind3 = bind(add2, _1, 100, _2);
cout << bind3(2, 3) << endl;
4.3绑定成员函数
绑定类的非静态成员函数时,必须显式指定对象(通过指针或引用):
#include <iostream>
#include <functional>class Person {
private:std::string name;
public:Person(std::string n) : name(n) {}// 成员函数:打印问候语void greet(const std::string& message) {std::cout << name << " says: " << message << "\n";}
};int main() {Person alice("Alice");// 绑定成员函数:需要对象指针 + 成员函数地址auto greet_alice = std::bind(&Person::greet, // 成员函数地址(必须带&)&alice, // 对象指针(作为第一个参数)std::placeholders::_1 // 消息参数占位);greet_alice("Hello, World!"); // 等价于 alice.greet("Hello, World!")// 输出:Alice says: Hello, World!return 0;
}