一、列表初始化
1. C++98 传统的{ }
C++98 中,数组 和 结构体可以用 { } 进行初始化:
struct Point {
int _x;
int _y;
};
int main() {
int array1[] = {1,2,3,4,5};
int array2[5] = {0};
Point p = {1,2};
return 0;
}
2. C++11 中的{ }
- C++11 以后想一初始化方式,试图实现一切对象皆可用 { } 初始化,{ } 初始化也叫做
列表初始化。
- 内置类型支持,自定义类型也支持(自定义类型本质是类型转换,中间会产生临时对象,最后编译器优化了以后变成直接构造)
- { } 初始化的过程中,可以省略掉 =
- C++11 列表初始化的本意是想实现⼀个统一的初始化方式,其次他在有些场景下带来的不少便利,如容器 push/inset 多参数构造的对象时,{ } 初始化会很方便
#include <iostream>
#include <vector>
using namespace std;
struct Point {
int _x;
int _y;
};
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {
cout << "Date(int year, int month, int day)" << endl;
}
Date(const Date &d) : _year(d._year), _month(d._month), _day(d._day) {
cout << "Date(const Date& d)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
int a1[] = {1,2,3,4,5};
int a2[5] = {0};
Point p = {1,2};
int x1 = {2};
Date d1 = {2025,1,1};
const Date &d2 = {2024,7,25};
Date d3 = {2025};
Date d4 = 2025;
Point p1{1,2};
int x2{2};
Date d5{2024,7,25};
const Date &d6{2024,7,25};
vector<Date> v;
Date d7 = {2002,1,5};
v.push_back(d7);
v.push_back(Date(2002,1,5));
v.push_back({2025,1,1});
return 0;
}
3. C++11 中的 std::initializer_list
- 上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如一个 vector 对象,我想用 N 个值去构造初始化,那么我们得实现很多个构造函数才能支持, vector< int > v1 = {1,2,3};vector< int > v2 = {1,2,3,4,5};
- C++11 库中提出了一个
std::initializer_list 的类
(1) std::initializer_list<int> il = {10, 20, 30};
(2) auto il = {10, 20, 30};
这个类的本质是底层创建一个 数组,将数据拷贝过来,std::initializer_list 内部有两个指针分别指向数组的开始和结束。
STL 容器支持一个 std::initializer_list 的构造函数,也就支持任意多个值构成的 {x1, x2, x3…} 进行初始化。STL 中的容器支持任意多个值构成的 {x1, x2, x3…} 进行初始化,就是通过 std::initializer_list 的构造函数支持的。
vector(initializer_list<value_type> il);
list(initializer_list<value_type> il);
map(initializer_list<value_type> il, const key_compare &comp = key_compare());
vector &operator=(initializer_list<value_type> il);
map &operator=(initializer_list<value_type> il);
// 模拟实现 vector 类中构造函数的 initializer_list 版本
template<class T>
class vector {
public:
typedef T *iterator;
vector(initializer_list<T> il) {
for(auto e : il)
push_back(e);
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
};
(1)示例一: initializer_list 对象 底层是在栈上创建一个数组
#include <iostream>
using namespace std;
int main() {
initializer_list<int> mylist;
mylist = {10,20,30};
int i = 0;
cout << mylist.begin() << endl;
cout << mylist.end() << endl;
cout << &i << endl;
return 0;
}
(2)示例二:
#include <iostream>
#include <vector>
#include <string>
#include <map>
using namespace std;
int main() {
vector<int>v1({1,2,3,4,5});
vector<int> v2 = {1,2,3,4,5};
vector<int> v3{1,2,3,4,5};
const vector<int>&v4 = {1,2,3,4,5};
map<string, string> dict = {{,},{,}};
v1 = {,,,,};
dict = {{,},{,}};
;
}
- 注意以下两种写法的区别:
// 有编译器优化的情况下,两种写法最终的效果无区别
(1)直接构造 vector<int>v1({1,2,3,4,5});
(2)构造 vector<int> 类的 临时对象 + 临时对象拷贝构造 v2; 编译器优化后,变为直接构造
vector<int> v2{1,2,3,4,5};
二、右值引用
1. 左值和右值
- 左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时 const 修饰符后的左值,不能给他赋值,但是可以取它的地址。
- 右值也是⼀个表示数据的表达式,要么是
字面值常量 、要么是表达式求值过程中创建的 临时对象 及 匿名对象 等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
- 值得一提的是,左值的英文简写为 lvalue,右值的英文简写为 rvalue。传统认为它们分别是 left value、right value 的缩写。现代 C++ 中,
lvalue 被解释为 loactor value 的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象;而 rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址 ,例如:临时变量,字面量常量,存储于寄存器中的变量等,也就是说 左值和右值的核心区别就是能否取地址。
#include <iostream>
using namespace std;
int add(int x, int y) {
int z = x + y;
return z;
}
int main() {
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;
add(x, y);
string("11111");
;
}
补充(临时对象产生的情形):
(1)产生运算的表达式结果存储在临时对象
(2)在类型转换过程中会产生临时对象存储中间值
(3)除返回值类型为指针类型或引用类型之外,其它返回类型(void 除外)在返回过程中都会创建临时对象
2. 左值引用和右值引用(讨论 右值引用变量 的属性)
-
Type& r1 = x;
Type&& rr1 = y;
第一个语句就是左值引用,左值引用就是给左值取别名;第二个就是右值引用,同样的道理,右值引用就是给右值取别名。
-
左值引用不能直接引用右值,但是 const 左值引用可以引用右值
-
右值引用不能直接引用左值,但是右值引用可以引用 move(左值)
-
move 是库里面的⼀个 函数模板,本质内部是进行强制类型转换(将左值强转成右值引用类型)
-
需要注意的是 变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量 的变量表达式的属性是左值
-
语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看下面代码中 r1 和 rr1 汇编层实现,底层都是用指针实现的,没什么区别。 底层汇编等实现和上层语法表达的意义有时是背离的,所以不要混到⼀起去理解,互相佐证,这样反而是陷入迷途
(1)示例一: 左值引用就是给左值取别名,右值引用就是给右值取别名。左值引用不能直接引用右值,但是 const 左值引用可以引用右值;右值引用不能直接引用左值,但是右值引用可以引用 move(左值)
#include <string>
using namespace std;
int add(int x, int y) {
int z = x + y;
return z;
}
int main() {
int*p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0]='x';
int&r1 = b;
int*&r2 = p;
int&r3 =*p;
string &r4 = s;
char&r5 = s[0];
int&&rr1 = 10;
double&&rr2 = x + y;
double&&rr3 = add(x, y);
string &&rr4 = string("11111");
const int&rx1 = 10;
const double&rx2 = x + y;
const double&rx3 = (x, y);
string &rx4 = ();
&&rrx1 = (b);
*&&rrx2 = (p);
&&rrx3 = (*p);
&&rrx4 = (&&)*p;
string &&rrx5 = (s);
string &&rrx6 = (string &&)s;
;
}
// move(左值):其实就相当于将 左值 强制转换成 右值引用类型
string s("111111");
string &&rrx5 = move(s);
string &&rrx6 = (string &&)s;
(2)示例二: 变量表达式都是左值属性,也就意味着 一个右值被右值引用绑定后,右值引用变量 的变量表达式的属性是左值
using namespace std;
int main() {
int b = 1;
int&r1 = b;
int&pr1 = r1;
int&&rr1 = 10;
int&&prr1 = move(rr1);
int&&prr2 = (int&&)rr1;
return 0;
}
// 编译器故意为之的设计,让右值引用变量 具有左值属性,就能直接修改引用的内容,正是这个特性创造了 移动语义!
弊端就是 右值引用变量 具有左值属性,不能再被右值引用绑定,除非 move 一下(或强制类型转换成 右值引用类型)
- 这里最让人疑惑的点是:rr1 不是本来就是右值引用类型嘛?rr1 直接被右值引用绑定就会报错
将 rr1 强制类型转换成 右值引用类型,再被右值引用绑定就可以
这也太神奇了,将右值引用类型 强制类型转换成 右值引用类型,强制类型转换前后有区别嘛???
也不需要太纠结,记住以下结论即可:强制类型转换成右值引用类型 的变量 具有右值属性;而普通的右值引用变量 具有左值属性
int&&rr1 = 10;
int&&prr1 = move(rr1);
int&&prr2 = (int&&)rr1;
3. 引用延长生命周期
右值引用可用于为 临时对象/匿名对象 延长生命周期;const 的左值引用也能延长 临时对象/匿名对象 生命存期,但这些对象无法被修改。
#include <iostream>
#include <string>
using namespace std;
int main() {
string s1 = "Test";
const string &r2 = s1 + s1;
std::string &&r3 = s1 + s1;
cout << r3 << endl;
r3 += "Test";
cout << r3 << endl;
return 0;
}
4. 左值和右值的参数匹配
- C++98 中,我们实现一个 const 左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
- C++11 以后,分别重载左值引用、const 左值引用、右值引用作为形参的 f 函数,那么实参是左值会匹配 f(左值引用),实参是 const 左值会匹配 f(const 左值引用),实参是右值会匹配 f(右值引用)。
- 右值引用变量在用于表达式时属性是左值(强制类型转换成右值引用类型 的变量 具有右值属性;而普通的右值引用变量 具有左值属性)
(1)示例一: 当只重载了 左值引用 和 const 左值引用作为形参的 f 函数时,实参是左值会匹配 f(左值引用),实参是右值 或 const 左值 会匹配 f(const 左值引用)
#include <iostream>
using namespace std;
void f(int&x) {
std::cout << "左值引用重载 f(int&)" << endl;
}
void f(const int&x) {
std::cout << "到 const 的左值引用重载 f(const int&)" << endl;
}
int main() {
int i = 1;
const int ci = 2;
f(i);
f(ci);
f(3);
f(move(i));
int&&x = 1;
f(x);
f(move(x));
return 0;
}
(2)示例二: 当分别重载了 左值引用、const 左值引用、右值引用作为形参的 f 函数时,那么实参是左值会匹配 f(左值引用),实参是 const 左值会匹配 f(const 左值引用),实参是右值会匹配 f(右值引用)
#include <iostream>
using namespace std;
void f(int&x) {
std::cout << "左值引用重载 f(int&)" << endl;
}
void f(const int&x) {
std::cout << "到 const 的左值引用重载 f(const int&)" << endl;
}
void f(int&&x) {
std::cout << "右值引用重载 f(int&&)" << endl;
}
int main() {
int i = 1;
const int ci = 2;
f(i);
f(ci);
f(3);
f(move(i));
int&&x = 1;
f(x);
f(move(x));
return 0;
}
三、移动语义
1. 左值引用的使用场景 及其 局限性(有些场景必须 传值返回)
左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。
左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如 addStrings 和 generate 函数,C++98 中的解决方案只能是被迫使用输出型参数解决。
那么 C++11 以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法挽救对象已经析构销毁的事实。
函数中创建的局部对象 无法 传引用返回,因为函数结束这个对象就析构销毁了,引用的对象都被销毁了,传引用返回就毫无意义了(函数中创建的局部对象 只能 传值返回)
#include <algorithm>
#include <vector>
#include <string>
using namespace std;
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;
}
vector<vector<int>>generate(int numRows) {
vector<vector<int>>vv(numRows);
for(int i = 0; i < numRows;++i){
vv[i].(i ,);
}
( i = ; i < numRows;++i){
( j = ; j < i;++j){
vv[i][j]= vv[i ][j]+ vv[i ][j ];
}
}
vv;
}
2. 移动构造 和 移动赋值(移动构造 与 拷贝构造 的对比)
- 移动构造函数是⼀种构造函数,类似拷贝构造函数,移动构造函数要求第⼀个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
- 移动赋值是⼀个赋值运算符的重载,他跟拷贝赋值构成函数重载,移动赋值函数要求第⼀个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
- 对于像 string/vector 这样的深拷贝的类 或者 包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引用的类型,他的本质是要'窃取'引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。 下面的 my_string 类(模拟实现 string 类)样例实现了移动构造和移动赋值,我们需要结合场景理解。
(1)模拟实现 STL 中的 string 类(只实现了部分接口):
// my_string.hpp
#include <iostream>
#include <string.h>
#include <algorithm>
#include <assert.h>
using namespace std;
class my_string {
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin(){return _str;}
iterator end(){return _str + _size;}
const_iterator begin()const{return _str;}
const_iterator end()const{return _str + _size;}
my_string(const char*str =""):_size(strlen(str)),_capacity(_size){
cout <<"string(char* str)-构造"<< endl;
_str =new char[_capacity +1];
strcpy(_str, str);
}
void swap(my_string &s){
std::swap(_str, s._str);
std::(_size, s._size);
std::(_capacity, s._capacity);
}
( my_string &s){
cout <<<< endl;
(s._capacity);
( ch : s){(ch);}
}
(my_string &&s){
cout <<<< endl;
(s);
}
my_string &=( my_string &s){
cout <<<< endl;
(!=&s){
_str[]=;
_size =;
(s._capacity);
( ch : s){(ch);}
}
*;
}
my_string &=(my_string &&s){
cout <<<< endl;
(s);
*;
}
~(){
cout <<<< endl;
[] _str;
_str =;
}
{
(n > _capacity){
*tmp = [n ];
(_str){(tmp, _str);[] _str;}
_str = tmp;
_capacity = n;
}
}
{
(_size >= _capacity){
newcapacity = _capacity ==?: _capacity *;
(newcapacity);
}
_str[_size]= ch;
++_size;
_str[_size]=;
}
&[]( pos){
(pos < _size);
_str[pos];
}
my_string &+=( ch){(ch);*;}
{ _size;}
:
*_str =;
_size =;
_capacity =;
};
对于像 string/vector 这样的深拷贝的类 或者 包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引用的类型,他的本质是要'窃取'引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率
移动构造的参数是右值引用,只能接收 临时对象等右值;
临时对象等右值的生命周期只有一行,下一行就会被销毁,所以通过移动构造 调用 swap 函数,直接快速转移临时对象 中的内容更为高效,而且还不用担心改变临时对象中内容会造成影响(临时对象下一行就会被销毁,对后续操作无影响)
结论:在移动构造产生前,用 string 临时对象 构造 string 对象 只能通过拷贝构造;
有了移动构造后,用 string 临时对象 构造 string 对象 通过移动构造,string 临时对象下一行就会销毁,直接转移 string 临时对象中内容更为高效!!!
3. 右值引用 和 移动语义 解决传值返回问题
#include"my_string.hpp"
my_string addStrings(my_string num1, my_string num2){
my_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;
}
3.1 右值对象构造场景
(1)右值对象构造,只有拷贝构造,没有移动构造的场景(删除 my_string 类中移动构造)
- 下图,左边为不优化的情况(linux 下,关闭 g++编译器的构造优化)下,两次拷贝构造;
右边为编译器优化的场景(vs2019 debug 环境下编译器对拷贝的优化)下连续步骤中的拷贝合二为一变为一次拷贝构造。
// linux 下可以将下面代码拷贝到 test.cpp 文件,编译时用 g++ test.cpp -fno-elide-constructors 的方式关闭 g++编译器的构造优化
(2)右值对象构造,有拷贝构造,也有移动构造的场景
- 下图,左边为不优化的情况(linux 下,关闭 g++编译器的构造优化)下,两次移动构造;
右边为编译器优化的场景(vs2019 debug 环境下编译器对拷贝的优化)下连续步骤中的拷贝合二为一变为一次移动构造。
str 对象不是左值嘛?为啥会通过 移动构造来 构造临时对象?
左边关闭了编译器的构造优化,但还有一些特殊的构造优化无法被关闭。
比如此处, 便是编译器特殊的构造优化的效果:str 是函数中创建的局部对象,在 return str 语句结束后,str 会被销毁回收;
虽然 str 是左值,但编译器认为在执行 return str 语句时,str 和 右值 很像,都是即将被销毁的对象,可以直接通过 移动构造 转移 str 对象的内容,所以 编译器在编译时,move 了此处的 str 对象,将 str 强制转换成右值属性, 这样运行时 执行到这条语句时,move(str) 会通过 移动构造来 构造临时对象。
- 需要注意的是在 vs2019 的 release 和 vs2022 的 debug 和 release 版本,下面代码优化会非常恐怖,会直接将 str 对象的构造,str 拷贝构造临时对象,临时对象拷贝构造 ret 对象,合三为一,变为直接构造(编译器优化效果:str 是对 ret 对象的引用,addStrings 函数中对 str 引用修改,实际上是直接对 ret 对象修改)。 要理解这个优化要结合局部对象生命周期和栈帧的角度理解,如下图所示
3.2 右值对象赋值场景
(1)右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景(删除 my_string 类中移动构造 和 移动赋值)
- 下图,左边展示了 vs2019 debug 模式下 和 关闭 g++编译器的构造优下,编译器的处理(这两种场景的结果一样,这证明 vs2019 debug 模式对 右值对象赋值的优化效果 为零):一次拷贝构造,一次拷贝赋值。
右边是在 vs2019 的 release 和 vs2022 的 debug 和 release 版本下,下面代码会进一步优化,直接构造要返回的临时对象,str 本质是临时对象的引用,addStrings 函数中对 str 引用修改,实际上是直接对临时对象修改。 运行结果的角度,我们可以看到 str 的析构是在赋值以后,说明 str 就是临时对象的别名。
(2)右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
- 下图,左边展示了 vs2019 debug 模式下 和 关闭 g++编译器的构造优下,编译器的处理(这两种场景的结果一样,这证明 vs2019 debug 模式对 右值对象赋值的优化效果 为零):一次移动构造,一次移动赋值。
右边是在 vs2019 的 release 和 vs2022 的 debug 和 release 版本下,下面代码会进一步优化,直接构造要返回的临时对象,str 本质是临时对象的引用,addStrings 函数中对 str 引用修改,实际上是直接对临时对象修改。 运行结果的角度,我们可以看到 str 的析构是在赋值以后,说明 str 就是临时对象的别名。
4. 右值引用 和 移动语义在传参中的提效(STL 容器的 push 和 insert 系列接口 增加右值引用版本)
- 查看 STL 文档,我们发现 C++11 以后 容器的 push 和 insert 系列的接口 增加了右值引用版本
- 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象
- 当实参是一个右值,容器内部则调用移动构造,将右值对象的资源转移到容器空间中的对象
- 以示例中模拟实现的 my_list 类(模拟实现 STL 中的 list 类)为例,支持右值引用参数版本的 push_back 和 insert
template<class T>
struct ListNode {
ListNode<T>*_next;
ListNode<T>*_prev;
T _data;
ListNode(const T &data =T()):_next(nullptr),_prev(nullptr),_data(data)
ListNode(T &&data):_next(nullptr),_prev(nullptr),_data(move(data))
{}
};
template<class T,class Ref,class Ptr>
struct ListIterator {
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node *_node;
ListIterator(Node *node):_node(node){}
Self &operator++(){
_node = _node->_next;
return*this;
}
Self &operator--(){
_node = _node->_pre;
return*this;
}
Ref operator*(){return _node->_data;}
};
template<class T>
class my_list {
typedef ListNode<T> Node;
void empty_init{
_head = ();
_head->_next = _head;
_head->_prev = _head;
}
:
(){();}
ListIterator<T, T &, T *> iterator;
ListIterator<T, T &, T *> const_iterator;
{ (_head->_next);}
{ (_head);}
{((), x);
{((),(x));
{
Node *cur = pos._node;
Node *newnode = (x);
Node *prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
(newnode);
}
{
Node *cur = pos._node;
Node *newnode = ((x));
Node *prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
(newnode);
}
:
Node *_head;
};
四、左值引用 和 右值引用的补充知识
1. 类型分类(了解即可)
- C++11 以后,进一步对类型进行了划分,右值被划分
纯右值(pure value,简称 prvalue)和 将亡值(expiring value,简称 xvalue)。
- 纯右值是指那些 字面值常量 或 求值结果相当于字面值或是一个不具名的临时对象。 如:42、true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调用,或者整形 a、b,a++,a+b 等。纯右值和将亡值 C++11 中提出的,C++11 中的纯右值概念划分等价于 C++98 中的右值。
- 将亡值是指 返回右值引用的函数的调用表达式 和 转换为右值引用的转换函数的调用表达,如 move(x)、static_cast<X&&>(x)、(X&&)x
- 泛左值(generalized value,简称 glvalue),泛左值包含
将亡值和 左值。
2. 引用折叠
2.1 引用折叠的介绍 和 规则(typedef 中的类型操作 构成引用的引用 的场景)
- C++ 中不能直接定义引用的引用,如 int& && r = i; ,这样写会直接报错。但通过
函数模板 或 typedef 中的类型操作 可以构成引用的引用。
- 通过模板或 typedef 中的类型操作可以构成引用的引用时,这时 C++11 给出了一个 引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
typedef int& lref;
typedef int&& rref;
int main() {
int n = 0;
lref &r1 = n;
lref &&r2 = n;
rref &r3 = n;
rref &&r4 = 1;
return 0;
}
2.2 函数模板 构成引用的引用 的场景(实现 万能引用)
- 像示例一中 f2 这样的函数模板中,T&& x 参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做
万能引用。
- Function(T&& t) 函数模板程序中,假设 实参是 int 右值,模板参数 T 的推导 int;实参是 int 左值,模板参数 T 的推导 int&。再结合引用折叠规则,就实现了实参是右值,实例化出右值引用版本形参的 Function;实参是左值,实例化出左值引用版本形参的 Function。
(1)示例一: 函数模板显示实例化
template<class T>
void f1(T &x){}
template<class T>
void f2(T &&x){}
int main() {
int n = 0;
f1<int>(n);
f1<int>(0);
f1<int&>(n);
f1<int&>(0);
f1<int&&>(n);
f1<int&&>(0);
f1<const int&>(n);
f1<const int&>(0);
f1<const int&&>(n);
f1<const int&&>(0);
<>(n);
<>();
<&>(n);
<&>();
<&&>(n);
<&&>();
;
}
(2)示例二: Function(T&& t) 函数模板程序中,假设 实参是 int 右值,模板参数 T 的推导 int;实参是 int 左值,模板参数 T 的推导 int&。再结合引用折叠规则,就实现了实参是右值,实例化出右值引用版本形参的 Function;实参是左值,实例化出左值引用版本形参的 Function
// 函数模板中的 T&& t 被称为 万能引用
#include <iostream>
using namespace std;
template<class T>
void Function(T &&t){
int a = 0;
T x = a;
cout << &a << endl;
cout << &x << endl;
}
int main() {
Function(10);
int a;
Function(a);
Function(move(a));
const int b = 8;
Function(b);
Function(move(b));
return 0;
}
3. 完美转发(搭配 万能引用 使用)
万能引用:Function(T&& arg) 函数模板程序中,传左值实例化以后是左值引用的 Function 函数,传右值实例化以后是右值引用的 Function 函数。
- 变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说 Function 函数中 arg 的属性是左值,那么我们把 arg 传递给下一层函数 wrapper,那么匹配的都是左值引用版本的 wrapper 函数。这里我们想要恢复 arg 对象的属性,就需要使用完美转发实现。
完美转发 forward本质是⼀个 函数模板,他主要还是通过引用折叠的方式实现,下面示例中传递给 Function 的实参是右值,T 被推导为 int,没有折叠,forward 内部 arg 被强制类型转换为右值引用返回;传递给 Function 的实参是左值,T 被推导为 int&,引用折叠为左值引用,forward 内部 arg 被强转为左值引用返回。
(1) 如果没有完美转发,左值引用类型 arg 和 右值引用类型 arg 都是左值属性,全会执行左值引用版本的 wrapper 函数
#include <iostream>
using namespace std;
template<typename T>
void wrapper(T &&arg){
}
template<>
void wrapper<int&>(int&arg){
cout <<"左值引用版本"<< endl;
}
template<>
void wrapper<int>(int&&arg){
cout <<"右值引用版本"<< endl;
}
template<typename T>
void function(T &&arg){
wrapper(arg);
}
int main() {
int x = 10;
function(x);
function(20);
return 0;
}
(2) 完美转发 forward 的效果:使 右值引用类型 arg 恢复右值属性!!!
有了完美转发,左值引用类型 arg 会执行左值引用版本的 wrapper 函数;右值引用类型 arg 会执行右值引用版本的 wrapper 函数
#include <iostream>
using namespace std;
template<typename T>
void wrapper(T &&arg){
}
template<>
void wrapper<int&>(int&arg){
cout <<"左值引用版本"<< endl;
}
template<>
void wrapper<int>(int&&arg){
cout <<"右值引用版本"<< endl;
}
template<typename T>
void function(T &&arg){
wrapper(forward<T>(arg));
}
int main() {
int x = 10;
function(x);
function(20);
return 0;
}
功能:根据模板参数 T 的类型,决定是否将参数 强制类型转换 为右值引用。
若 T 是左值引用(如 int&),返回左值引用。
若 T 是非引用或右值引用(如 int 或 int&&),返回 强制类型转换的右值引用(等效于 move 操作)。
五、可变参数模板
1. 基本语法及原理
- C++11 支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。
(1)template<class...Args>
void Func(Args... args){}
(1)template<class...Args>
void Func(Args&... args){}
(1)template<class...Args>
void Func(Args&&... args){}
- 我们用省略号来指出⼀个模板参数或函数参数的表示一个包,在模板参数列表中,class… 或 typename…指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟…指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
- 可变参数模板的原理跟模板类似,本质 还是去实例化对应类型和个数的多个函数。
(1) 使用 sizeof...运算符去计算参数包中参数的个数
#include <iostream>
using namespace std;
template<class... Args>
void Print(Args &&...args){
cout << sizeof...(args) << endl;
}
int main() {
double x = 2.2;
Print();
Print(1);
Print(1,string("xxxxx"));
Print(1.1,string("xxxxx"), x);
return 0;
}
可变参数模板的原理跟模板类似,本质 还是去实例化对应类型和个数的多个函数。如下:
void Print();
template <class T1> void Print(T1&& arg1);
template <class T1, class T2> void Print(T1&& arg1, T2&& arg2);
template <class T1, class T2, class T3> void Print(T1&& arg1, T2&& arg2, T3&& arg3);
2. 包扩展
- 对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供用于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(…)来触发扩展操作。底层的实现细节如 示例一的图解所示。
- C++ 还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。
(1)示例一: 第一种包扩展方式
#include <iostream>
using namespace std;
void ShowList(){
cout << endl;
}
template<class T,class... Args>
void ShowList(T x, Args... args){
cout << x <<" ";
ShowList(args...);
}
template<class... Args>
void Print(Args... args){
ShowList(args...);
}
int main() {
Print(1,string("xxxxx"),2.2);
return 0;
}
(2)示例二: 第二种包扩展方式
#include <iostream>
using namespace std;
template<class T>
const T &GetArg(const T &x){
cout << x <<" ";
return x;
}
template<class... Args>
void Arguments(Args... args){}
template<class... Args>
void Print(Args... args){
Arguments(GetArg(args)...);
}
int main() {
Print(1,string("xxxxx"),2.2);
return 0;
}
3. emplace 系列接口(emplace 系列接口 比 insert 和 push 系列接口高效)
- C++11 以后 STL 容器新增了 emplace 系列的接口,emplace 系列的接口均为
模板可变参数, 功能上兼容 push 和 insert 系列,但是 emplace 还支持新玩法, 假设容器为 container,emplace 还支持直接插入构造 T 对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造 T 对象。
template<class... Args>
void emplace_back(Args&&... args);
template<class... Args>
iterator emplace(const_iterator position, Args&&... args);
- emplace_back 总体而言是更高效,推荐以后使用 emplace 系列 替代 insert 和 push 系列
- 传递参数包过程中,如果是 Args&&… args(万能引用)的参数包,要用完美转发参数包,方式如下: std::forward(args)…,否则编译时包扩展后右值引用变量表达式就变成了左值。
- 我们模拟实现了 list 的 emplace 和 emplace_back 接口,这里把参数包不断往下传递,最终在结点的构造中直接去匹配容器存储的数据类型 T 的构造,所以达到了前面说的 emplace 支持直接插入构造 T 对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造 T 对象。
// my_list.h (补充 emplace_back 接口 的实现)
template<class T>
struct ListNode {
ListNode<T>*_next;
ListNode<T>*_prev;
T _data;
ListNode(const T &data =T()):_next(nullptr),_prev(nullptr),_data(data){}
ListNode(T &&data):_next(nullptr),_prev(nullptr),_data(move(data)){}
template<class... Args>
:_next(nullptr),_prev(nullptr),_data(std::forward<Args>(args)...){}
};
template<class T,class Ref,class Ptr>
struct ListIterator {
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node *_node;
ListIterator(Node *node):_node(node){}
Self &operator++(){
_node = _node->_next;
return*this;
}
Self &operator--(){
_node = _node->_pre;
return*this;
}
Ref operator*(){return _node->_data;}
};
template<class T>
{
ListNode<T> Node;
{
_head = ();
_head->_next = _head;
_head->_prev = _head;
}
:
(){();}
ListIterator<T, T &, T *> iterator;
ListIterator<T, T &, T *> const_iterator;
{ (_head->_next);}
{ (_head);}
{((), x);}
{((),(x));}
{
Node *cur = pos._node;
Node *newnode = (x);
Node *prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
(newnode);
}
{
Node *cur = pos._node;
Node *newnode = ((x));
Node *prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
(newnode);
}
<... Args>
{
((), std::forward<Args>(args)...);
}
{
Node *cur = pos._node;
Node *newnode = (std::forward<Args>(args)...);
Node *prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
(newnode);
}
:
Node *_head;
};
(1)示例一: 前两种情况,push_back 和 emplace_back 效率一样;第三种情况,emplace_back 效率更高,所以 emplace_back 总体而言是更高效的
#include"my_list.h"
#include <string>
using namespace std;
int main() {
my_list<string> lt;
string s1("111111111111");
lt.emplace_back(s1);
lt.emplace_back(move(s1));
lt.emplace_back("111111111111");
return 0;
}
(2)示例二: 前两种情况,push_back 和 emplace_back 效率一样;第三种情况,emplace_back 效率更高,所以 emplace_back 总体而言是更高效的
#include"my_list.h"
#include <string>
using namespace std;
int main() {
my_list<pair<string,int>> lt1;
pair<string,int>kv("苹果",1);
lt1.emplace_back(kv);
lt1.emplace_back(move(kv));
lt1.emplace_back("苹果",1);
return 0;
}
六、新的类功能
1. 默认的移动构造和移动赋值(先看 三.2)
- 原来 C++ 类中,有 6 个默认成员函数:构造函数 / 析构函数 / 拷贝构造函数 / 拷贝赋值重载 以及 取地址重载 / const 取地址重载,最重要的是前 4 个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
- 如果你没有自己实现移动构造函数,且没有显式实现析构函数、拷贝构造、拷贝赋值重载中的任何⼀个。那么编译器会自动生成⼀个默认的移动构造。
默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝;对于自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有显式实现析构函数、拷贝构造、拷贝赋值重载中的任何⼀个,那么编译器会自动生成一个默认的移动赋值。
默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝;对于自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果显式实现了移动构造 或 移动赋值中的任意一个,当用户未显示实现拷贝构造 和 拷贝赋值时,编译器不会自动生成默认的拷贝构造 和 拷贝赋值。
2. default 和 delete
- C++11 可以让你更好的控制要使用的默认函数。 假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。 比如:我们显示实现了拷贝构造,编译器就不会生成默认的移动构造了,那么我们 可以使用 default 关键字 显式指定 编译器生成默认的移动构造
- 如果能想要限制某些默认函数的生成,在 C++98 中,是该函数设置成 private,这样只要其他人想要调用就会报错。
在 C++11 中更简单,只需在该函数声明加上=delete 即可,该语法指示编译器不生成对应函数的默认版本,称=delete 修饰的函数为删除函数。
(1)示例一: 我们显示实现了拷贝构造,编译器就不会生成默认的移动构造了,那么我们可以使用 default 关键字 显式指定 编译器生成默认的移动构造
#include <string>
using namespace std;
class Person {
public:
Person(const char*name ="",int age =0):_name(name),_age(age){}
Person(const Person &p):_name(p._name),_age(p._age){}
Person(Person &&p)=default;
private:
string _name;
int _age;
};
(2)示例二: 如果能想要限制某些默认函数的生成,只需在该函数声明加上=delete 即可,该语法指示编译器不生成对应函数的默认版本,称=delete 修饰的函数为删除函数
#include <string>
using namespace std;
class Person {
public:
Person(const char*name ="",int age =0):_name(name),_age(age){}
Person(const Person& p)=delete;
private:
string _name;
int _age;
};
3. final 与 override
final 关键字在 继承章节 详细介绍过,用于实现一个不能被继承的类。
override 关键字在 多态章节 详细介绍过,用于检查虚函数是否构成重写。
七、lambda
1. lambda 表达式语法
- lambda 表达式本质是一个
匿名函数对象,跟普通函数不同的是他可以定义在函数内部。
lambda 表达式语法使用层而言没有类型,所以我们一般是用 auto 或者 模板参数定义的对象去接收 lambda 对象。
lambda 表达式的格式: [capture-list] (parameters)-> return type { function boby }
[capture-list]: 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 [ ] 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉。捕捉列表为空也不能省略。
(parameters):参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略
->return type :返回值类型,用追踪返回类型形式声明函数的返回值类型。没有返回值时,此部分可省略;一般返回值类型明确情况下,也可省略,由编译器对返回值类型进行自动推导。
{function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
#include <iostream>
using namespace std;
int main() {
auto add1 =[](int x,int y)->int{return x + y;};
cout << add1(1,2) << endl;
auto func1 =[]{ cout <<"hello bit"<< endl;return 0;};
func1();
int a = 0, b = 1;
cout << a <<":"<< b << endl;
auto swap1 =[](int&x,int&y){int tmp = x; x = y; y = tmp;};
swap1(a, b);
cout << a <<":"<< b << endl;
return 0;
}
2. 捕捉列表
- lambda 表达式中默认只能使用 lambda 函数体 和 参数中的变量(参数列表接收的参数变量),如果想使用外层作用域中的变量就需要进行
捕捉
- 第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。如 [x,y,&z] 表示 x 和 y 值捕捉,z 引用捕捉。
- 第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表示隐式值捕捉,在捕捉列表写⼀个&表示隐式引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量。
- 第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=, &x] 表示其他变量隐式值捕捉,x 引用捕捉;[&, x, y] 表示其他变量引用捕捉,x 和 y 值捕捉。当使用混合捕捉时,第一个元素必须是 = 或 &,并且 & 混合捕捉时,后面的捕捉变量必须是值捕捉;同理 = 混合捕捉时,后面的捕捉变量必须是引用捕捉。
- lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使用。 这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
- 默认情况下, lambda 捕捉列表的传值捕捉是被 const 修饰的(传值捕捉本质是一种拷贝,并且被 const 修饰了),也就是说传值捕捉过来的对象不能修改,
mutable加在 参数列表 的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。 使用该修饰符后,参数列表不可省略 (即使参数为空)。
(1)第一种捕捉方式: 在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割
#include <iostream>
using namespace std;
int main() {
int a = 0, b = 0, c = 0, d = 0;
auto func1 =[a, b,&c,&d]{
c++;
d++;
};
func1();
cout << a <<'|'<< b <<'|'<< c <<'|'<< d << endl;
return 0;
}
(2) lambda 捕捉列表的传值捕捉是被 const 修饰的,也就是说传值捕捉过来的对象不能修改。mutable 加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略 (即使参数为空)。
#include <iostream>
using namespace std;
int main() {
int a = 0, b = 0, c = 0, d = 0;
auto func1 =[a, b,&c,&d]()mutable{
a++;
b++;
c++;
d++;
};
func1();
cout << a <<'|'<< b <<'|'<< c <<'|'<< d << endl;
return 0;
}
(3)第二种捕捉方式: 在捕捉列表中隐式捕捉(我们在捕捉列表写⼀个=表示隐式值捕捉;在捕捉列表写⼀个&表示隐式引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量)
#include <iostream>
using namespace std;
int main() {
int a = 0, b = 0, c = 0, d = 0;
auto func1 =[=]()mutable{
a++;
b++;
c++;
d++;
};
func1();
cout << a <<'|'<< b <<'|'<< c <<'|'<< d << endl;
auto func2 =[&]{
a++;
b++;
c++;
d++;
};
func2();
cout << a <<'|'<< b <<'|'<< c <<'|'<< d << endl;
return 0;
}
(4)第三种捕捉方式: 在捕捉列表中混合使用隐式捕捉和显示捕捉(当使用混合捕捉时,第一个元素必须是 = 或 &,并且 & 混合捕捉时,后面的捕捉变量必须是值捕捉;同理 = 混合捕捉时,后面的捕捉变量必须是引用捕捉)
#include <iostream>
using namespace std;
int main() {
int a = 0, b = 0, c = 0, d = 0;
auto func1 =[=,&c,&d]()mutable{
a++;
b++;
c++;
d++;
};
func1();
cout << a <<'|'<< b <<'|'<< c <<'|'<< d << endl;
auto func2 =[&, c, d]()mutable{
a++;
b++;
c++;
d++;
};
func2();
cout << a <<'|'<< b <<'|'<< c <<'|'<< d << endl;
return 0;
}
(5) lambda 表达式不能捕捉静态局部变量和全局变量,因为静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使用
#include <iostream>
using namespace std;
int m = 0;
int main() {
static int n = 0;
auto func1 =[]{
m++;
n++;
};
func1();
cout << m <<'|'<< n << endl;
return 0;
}
静态局部变量 和 全局变量 在程序启动时初始化,内存地址固定,生命周期持续至程序结束。
Lambda 表达式无需通过捕获列表显式捕获它们,编译器在编译时能直接解析其地址。
Lambda 表达式内:对静态/全局变量的修改是直接操作原始内存地址中内容,因此外部作用域的值会同步变化。
| 变量类型 | 捕获方式 | 内部修改是否影响外部 | 能否被捕获 |
|---|
| 非静态局部变量 | 值捕获 [x] | 不影响(副本) | 能 |
| 非静态局部变量 | 引用捕获 [&x] | 影响 | 能 |
| 静态局部变量 | 不捕获 | 影响 | 禁止 |
| 全局变量 | 不捕获 | 影响 | 禁止 |
3. lambda 的原理
- lambda 的原理和 范围 for 很像,编译后从汇编指令层的角度看,压根就没有 lambda 和 范围 for 这样的东西。范围 for 底层是迭代器,而 lambda 底层是
仿函数对象,也就说我们写了一个 lambda 以后,编译器会生成一个对应的仿函数的类。
- 仿函数的类名是编译按一定规则生成的,保证不同的 lambda 生成的类名不同,lambda 参数/返回类型/函数体就是仿函数 operator() 的参数/返回类型/函数体, lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。
#include <iostream>
using namespace std;
class Rate {
public:
Rate(int a,int b,int&c,int&d):_a(a),_b(b),_pc(c),_pd(d){}
int operator()(int ret)const{
_pc++;
_pd++;
ret = _a + _b + _pc + _pd;
return ret;
}
private:
int _a;
int _b;
int&_pc;
int&_pd;
};
int main() {
int a = 0, b = 0, c = 0, d = 0;
int ret = 0;
auto function_1 =[a, b,&c,&d](int ret){
c++;
d++;
ret = a + b + c + d;
return ret;
};
cout << function_1(ret) << endl;
cout << a <<'|'<< b <<'|'<< c <<'|'<< d << endl;
auto function_2 = Rate(a,b,c,d);
cout << function_2(ret) << endl;
cout << a <<'|'<< b <<'|'<< c <<'|'<< d << endl;
return ;
}
Lambda 表达式在编译期会被转换为一个 类(操作系统自动为每个 Lambda 表达式生成唯一的类名(也就是说每个 Lambda 表达式的类类型不同)),该类包含以下核心组件:
(1)operator() 重载:
Lambda 的函数体被编译为该类的 operator() 成员函数;
lambda 参数列表/返回类型/函数体 就是 仿函数 operator() 的参数/返回类型/函数体。
若无 mutable,operator() 为 const 成员函数(禁止修改值捕获的变量);
若添加 mutable,operator() 变为非 const(允许修改值捕获的副本)。
(2)成员变量存储捕获内容:
捕获列表中的变量会生成对应的类成员变量,捕获方式决定成员类型:
值捕获 → 生成普通成员变量(受 operator() 的 const 性约束)。
引用捕获 → 生成引用类型成员变量(T&)。
将 const 修饰的成员函数称之为 const 成员函数。 const 实际修饰该成员函数隐含的 this 指针,表明在该成员函数中不能对 类的任何成员变量本身的内容进行修改(但如果该成员变量是指针 或 引用类型,该成员变量仍可以对其指向 或 引用的内容进行修改,不受任何影响)
const 修饰 Rate 类的 operator() 成员函数,operator() 隐含的 this 指针由 Date * const this 变为 const Date * const this
所以在运行时,Lambda 表达式实际上就是一个类对象(匿名对象)
4. lambda 的使用场景
- 在学习 lambda 表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用 lambda 去定义可调用对象,既简单又方便。
- lambda 在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等, lambda 的应用还是很广泛的,以后我们会不断接触到。
#include <string>
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
struct Goods {
string _name;
double _price;
int _evaluate;
Goods(const char*str,double price,int evaluate):_name(str),_price(price),_evaluate(evaluate){}
};
struct ComparePriceLess {
bool operator()(const Goods &gl,const Goods &gr){
return gl._price < gr._price;
}
};
struct ComparePriceGreater {
bool operator()(const Goods &gl,const Goods &gr){
return gl._price > gr._price;
}
};
bool CompEvaluateLess(const Goods &gl,const Goods &gr){
return gl._evaluate < gr._evaluate;
}
{
gl._evaluate > gr._evaluate;
}
{
vector<Goods> v ={{,,},{,,},{,,},{,,}};
(v.(), v.(),());
(v.(), v.(),());
(v.(), v.(), CompEvaluateLess);
(v.(), v.(), CompEvaluateGreater);
(v.(), v.(),[]( Goods &g1, Goods &g2){
g_price < g_price;
});
(v.(), v.(),[]( Goods &g1, Goods &g2){
g_price > g_price;
});
(v.(), v.(),[]( Goods &g1, Goods &g2){
g_evaluate < g_evaluate;
});
(v.(), v.(),[]( Goods &g1, Goods &g2){
g_evaluate > g_evaluate;
});
;
}
八、包装器
1. function
- std::function 是一个
类模板,也是一个包装器。
std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、 lambda、bind 表达式等,存储的可调用对象被称为 std::function 的目标。 若 std::function 不含目标,则称它为空。调用空 std::function 的目标导致抛出 std::bad_function_call 异常。
- 以下是 function 的原型,他被定义在 < functional > 头文件中。
template<class T>
class function;
template<class Ret,class... Args>
class function<Ret(Args...)>;
- 函数指针、仿函数、 lambda 等可调用对象的类型各不相同, std::function 的优势就是统⼀类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型,下面的第二个代码样例展示了 std::function 作为 map 的参数,实现字符串和可调用对象的映射表功能。
(1)示例一: function(包装器)对 函数指针、仿函数、 lambda 对象 以及 类的普通成员函数 和 静态成员函数 进行包装
#include <iostream>
#include <functional>
using namespace std;
int f(int a,int b){return a + b;}
struct Functor {
int operator()(int a,int b){return a + b;}
};
class Plus {
public:
static int plusi(int a,int b)
{return a + b;}
double plusd(double a,double b)
{return a + b;}
};
int main() {
function<int(int,int)> f1 = f;
function<int(int,int)> f2 = Functor();
function<(,)> f3 =[]( a, b){ a + b;};
cout << (,) << endl;
cout << (,) << endl;
cout << (,) << endl;
function<(,)> f4 =&Plus::plusi;
cout << (,) << endl;
function<(Plus *,,)> f5 =&Plus::plusd;
Plus pd;
cout << (&pd,,) << endl;
;
}
(2)示例二: 逆波兰表达式求值(function 的应用场景)
// 传统解法: 使用 STL 中的 stack 容器 + switch 语句
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();
}
};
// 进阶解法: 使用 STL 中的 stack 容器 + map 映射 string 和 function
// 这种方式的最大优势之一是方便扩展,假设还有其他运算,我们增加 map 中的映射即可
class Solution {
public:
int evalRPN(vector<string>&tokens){
stack<int> st;
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();
}
};
2. bind
- bind 是⼀个
函数模板,它也是⼀个可调用对象的包装器,可以把他看做⼀个函数适配器,对接收的 fn 可调用对象进行处理后返回一个可调用对象。bind 可以用来调整参数个数和参数顺序。
- 以下是 bind 的原型,他被定义在 < functional > 头文件中。
template<class Fn,class... Args>
bind(Fn&& fn, Args&&... args);
template<class Ret,class Fn,class... Args>
bind(Fn&& fn, Args&&... args);
- 调用 bind 的一般形式: auto newCallable = bind(callable,arg_list);
其中 newCallable 本身是一个可调用对象,callable 是 bind 要包装的可调用对象,arg_list 是一个逗号分隔的参数列表,对应给定的 callable 的参数。
当我们调用 newCallable 时,newCallable 会调用 callable,并传给它 arg_list 中的参数。
- arg_list 中的参数可能包含形如_n 的名字,其中 n 是一个整数,这些参数是
占位符,表示 newCallable 的参数,它们占据了传递给 newCallable 的参数的位置。 数值 n 表示生成的可调用对象中参数的位置:_1 为 newCallable 的第一个参数,_2 为第二个参数,以此类推。 _1/_2/_3… 这些占位符放到 placeholders的一个命名空间中。
(1)示例一: bind 调整可调用对象的 参数顺序
#include <iostream>
#include <functional>
using namespace std;
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int sub(int a,int b,int c){return(a - b - c)*10;}
int main() {
auto sub0 =bind(sub, _1, _2, _3);
cout << sub0(10,5,1) << endl;
auto sub1 =bind(sub, _2, _1, _3);
cout << sub1(10,5,1) << endl;
auto sub2 =bind(sub, _3, _2, _1);
cout << sub2(10,5,1) << endl;
return 0;
}
(2)示例二: bind 绑死可调用对象的一些固定参数,从而减少传参个数
#include <iostream>
#include <functional>
using namespace std;
using placeholders::_1;
using placeholders::_2;
int sub(int a,int b,int c){return(a - b - c)*10;}
class Plus {
public:
double plusd(double a,double b){return a + b;}
};
int main() {
auto sub1 =bind(sub,100, _1, _2);
cout << sub1(5,1) << endl;
auto sub2 =bind(sub, _1,100, _2);
cout << sub2(5,1) << endl;
auto sub3 =bind(sub, _1, _2,100);
cout << sub3(5,1) << endl;
cout <<"---------------------------"<< endl;
sub4 =(sub,,, _1);
cout << () << endl;
sub5 =(sub, _1,,);
cout << () << endl;
cout <<<< endl;
function<(Plus *,,)> f1 =&Plus::plusd;
Plus pd;
cout << (&pd,,) << endl;
function<(,)> f2 =(&Plus::plusd,&pd, _1, _2);
cout << (,) << endl;
;
}
(3)示例三: bind 的应用场景(在银行定期存款,3 年定期的利率为 1.5%,5 年定期的利率为 2%,10 年定期的利率为 2.5%,30 年定期的利率为 3.5%,每种定期存款的年数 与 利率 是固定的,只有存进去的金额在变化,所以可以使用 bind 绑死年数 与 利率的参数)
#include <iostream>
#include <functional>
using namespace std;
using placeholders::_1;
int main() {
auto func =[](double rate,double money,int year)->double{
double ret = money;
for(int i =0; i < year; i++){
ret += ret * rate;
}
return ret - money;
};
function<double(double)> func1 =bind(func,0.015, _1,3);
function<double(double)> func2 =bind(func,0.02, _1,5);
function<double(double)> func3 =bind(func,0.025, _1,10);
function<double(double)> func4 =bind(func,0.035, _1,30);
cout << func1(1000000.0) << endl;
cout << func2(1000000.0) << endl;
cout << func3(1000000.0) << endl;
cout << func4() << endl;
;
}
九、智能指针
本文暂不展开介绍智能指针的具体实现与细节,建议参考相关标准文档或深入学习资料。