C++ 11
列表初始化
C++98的{}
C++98中数组和结构体可以用 {} 初始化。
cpp
struct Point
{
int x;
int y;
}
int main()
{
int arr[]={1,2,3,4,5};
Point p={10,20};
return 0;
}
C++11的{}
- {}初始化即列表初始化。
- 内置类型 和自定义类型都支持列表初始化,自定义类型本质是类型转换,中间会产生临时对象,编译器优化了以后变成直接构造。
- {}初始化过程中,可以省略 = 。
cpp
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构造" << endl;
}
Date(const Date& d)
:_year(d._year)
,_month(d._month)
,_day(d._day)
{
cout << "Date拷贝构造" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
int x1 = { 2 };
Date d1 = { 2026,1,26 };
const Date& d2 = { 2026,1,25 };
//C++98支持单参数时类型转换,可以不用{}
Date d3 = { 2026 };
Date d4 = 2026;
//省略=
Point p1{ 1,2 };
int x2{ 2 };
Date d5{ 2026,1,25 };
const Date& d6{ 2026,1,26 };
vector<Date> v;
v.push_back(d1);
v.push_back(Date(2026, 1, 1));//匿名对象
v.push_back({ 2026,1,2 });
return 0;
}
std::initializer_list
C++11库中提出了⼀个std::initializer_list的类,支持迭代器遍历,其本质是底层开⼀个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。
cpp
int main()
{
initializer_list<int> il;
il = { 10,20,30 };
cout << sizeof(il) << endl;
int i = 0;
cout << il.begin() << endl;
cout << il.end() << endl;
cout << &i << endl; //initializer_list对象的两个指针与i的地址相近,说明其底层的数组在栈上
vector<int> v1({ 1,2,3,4,5 });
vector<int> v2 = { 6,7,8,9,10 };
const vector<int>& v3 = { 1,2,3,4,5,6,7,8,9 };
v1 = { 10,20,30,40,50 };
map<string, string> dict = { {"sort","排序"} ,{"string","字符串"} };
return 0;
}

右值引用与移动语义
C++11中新增了的右值引用 语法,无论左值引用还是右值引用,都是给对象取别名。
左值和右值
- 左值 是⼀个表示数据的表达式 (如变量名 或解引用的指针),一般是有持久状态 ,存储在内存中 ,可以获取它的地址,左值可以出现在赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
- 右值 也是⼀个表示数据的表达式 ,要么是字面值常量 、要么是表达式求值过程中 创建的临时对象 等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边 ,右值不能取地址。
- 左值和右值的核心区别即能否取地址。
cpp
int main()
{ //左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("aaaaa");
s[0] = 'x';
cout << &c << endl;
cout << (void*)s[0] << endl;//强转成void*才能取到s[0]的地址,否则会输出字符串
//右值
double x = 1.1, y = 2.2;
10;
x + y;
fmin(x, y);
string("abcdef");
//无法取地址
//cout << &10 << endl;
//cout << &(x + y) << endl;
return 0;
}

左值引用和右值引用
- 左值引用就是给左值取别名,右值引用就是给右值取别名。
- 左值引用不能直接引用右值,const左值引用可以引用右值。
- 右值引用不能直接引用左值,右值引用可以引用std::move(左值)。move是库里面的一个函数模板,本质是内部进行强制类型转换。
- 变量表达式都是左值属性,所以一个右值被右值引用绑定后 ,右值引用变量表达式的属性是左值。
- 左值引用和右值引用都是给变量取别名,不开空间,底层都是用指针实现的。
cpp
int main()
{
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("aaaaa");
s[0] = 'x';
double x = 1.1, y = 2.2;
10;
x + y;
fmin(x, y);
string("abcdef");
//左值引用
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 = fmin(x, y);
string&& rr4 = string("abcdef");
//const左值引用可以引用右值
const int& cr1 = 10;
const double& cr2 = x + y;
const double& cr3 = fmin(x, y);
const string& cr4 = string("abcdef");
//右值引用可以引用move(左值)
int&& mrr1 = move(b);
int*&& mrr2 = move(p);
int&& mrr3 = move(*p);
string&& mrr4 = (string&&)s;
string&& mrr5 = move(s);
//b、r1、rr1都是变量表达式,都是左值,可以取地址
cout << &b << endl;
cout << &r1 << endl;
cout << &rr1 << endl<<endl;
int& r6 = r1;
int&& mrr6 = move(rr1);//rr1是右值引用变量,是左值,需要用move转成右值才能被右值引用
cout << &r6 << endl;
cout << &mrr6 << endl;
return 0;
}

引用延长生命周期
右值引用可以为临时对象 延长生命周期。const 的左值引用也能延长临时对象生存期,但这些对象无法被修改。
cpp
int main()
{
string s1 = "test";
string&& rr1 = s1+s1;
const string& r1 = s1 + s1;
//r1 += "test";
rr1 += "test";
cout << rr1 << endl;
return 0;
}


左值和右值的参数匹配
- C++98中,const左值引用作为参数的函数,实参传递左值和右值都可以匹配。
- C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会匹配f(左值引用) ,实参是const左值会匹配f(const 左值引用) ,实参是右值会匹配f(右值引用)。
cpp
void f(int& x)
{
cout << "左值引用重载 f(" << x << ")\n";
}
void f(const int& x)
{
cout << "到 const 的左值引用重载 f(" << x << ")\n";
}
void f(int&& x)
{
cout << "右值引用重载 f(" << x << ")\n";
}
int main()
{
int i = 1;
const int ci = 2;
f(i);
f(ci);
f(3);
f(move(i));
int&& x = 1;
f(x);//调用形参为左值引用的f函数,因为右值引用变量x的属性为左值
f(move(x));
return 0;
}

右值引用和移动语义的使用场景
有些场景不能使用传左值引用返回,如addSrings和generate函数,当string、vector的对象很大时,函数传值返回的时候会有很大的拷贝开销,若传左值引用返回,局部变量str和vv出了函数后被销毁,就成了野引用。传右值引用返回也无济于事。
cpp
class Solution { //两个数组字符串相加
public:
// 传值返回需要拷贝
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;
}
};
class Solution { //打印杨辉三角
public:
// 这里的传值返回拷贝代价就太大了
vector<vector<int>> generate(int numRows)
{
vector<vector<int>> vv(numRows);
for(int i = 0; i < numRows; ++i)
{
vv[i].resize(i+1, 1);
}
for(int i = 2; i < numRows; ++i)
{
for(int j = 1; j < i; ++j)
{
vv[i][j] = vv[i-1][j] + vv[i-1][j-1];
}
}
return vv;
}
};
移动构造 :⼀种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的右值引用 ,如果还有其他参数,额外的参数必须有缺省值。
移动赋值 :⼀个赋值运算符的重载,与拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的右值引用。
对于像string/vector这样的深拷贝的类 或者包含深拷贝的成员变量的类 ,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引用的类型,本质是要"窃取"引用的右值对象的资源。
cpp
namespace mine
{
class 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;
}
const char* c_str()
{
return _str;
}
size_t size()
{
return _size;
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* t = new char[n+1];
if (_str)
{
strcpy(t, _str);
delete[] _str;
}
_str = t;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "构造string(const char* str = "")" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
~string()
{
//cout << "析构~string()" << endl;
delete[] _str;
_str = nullptr;
}
string(const string& s)
:_str(nullptr)
{
cout << "拷贝构造string(const string& s)" << endl;
reserve(s._capacity);
for (auto e : s)
{
push_back(e);
}
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string& operator=(const string& s)
{
cout << "拷贝赋值string& operator=(const string& s)" << endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto e : s)
{
push_back(e);
}
}
return *this;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
//*******************************************************
string(string&& s)
{
cout << "移动构造string(string&& s)" << endl;
swap(s);
}
string& operator=(string&& s)
{
cout << "移动赋值string& operator=(string&& s)" << endl;
swap(s);
return *this;
}
//*******************************************************
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}
int main()
{
mine::string s1("xxxxx");
cout << endl << endl;
mine::string s2 = s1;
cout << endl << endl;
mine::string s3 = mine::string("abcdefg");
cout << endl << endl;
mine::string s4 = move(s1);
cout << endl << endl;
return 0;
}



可以看到调用移动构造后,s1的资源给了s4,本质是交换了底层的指针,体现了"窃取"。
我们可以用右值引用和移动语义解决传值返回的问题。
对于addStrings函数,有两个场景。
cpp
//场景1
int main()
{
mine::string ret=mine::addStrings("11111","2222");
return 0;
}
//场景2
int main()
{
mine::string ret;
ret=mine::addStrings("11111","2222");
return 0;
}

对于场景1,如果没有移动构造,str需要把资源拷贝给临时对象,str出函数销毁会后临时对象再把资源拷贝给ret。若对象str很大,两次拷贝的开销也会很大,有了移动构造后,通过交换底层的指针,str的资源转移到了临时对象,再交换指针,临时对象的资源又转移到了ret,这样就减少了拷贝。

对于场景2,如果没有移动构造和移动赋值,str要先把资源拷贝给临时对象,临时对象再赋值给ret,赋值过程中又要遍历字符串,所以str很大,消耗也会很大。有了移动构造和移动赋值后,str通过交换底层的指针把资源转移到临时对象,移动赋值时也是交换底层指针把资源转移到ret,这样就减少了拷贝。
C++11以后容器的push和insert系列的接口增加的右值引用版本,当实参是⼀个左值 时,容器内部继续调用拷贝构造 进行拷贝,将对象拷贝到容器空间中的对象。当实参是⼀个右值 ,容器内部则调用移动构造,右值对象的资源到容器空间的对象上。
总结:
- 左值引用和右值引用最终目的减少拷贝,提高效率。
- 左值引用可以修改参数/返回值,方便使用。
左值引用的不足:部分函数场景,只能传值返回,不能左值引用返回。当前函数局部对象出了函数作用域就销毁,此时不能左值引用返回,只能传值返回。
解决方案:
-
不用返回值,用输出型参数解决,会牺牲可读性。
cppvector<vector<int>> ret; Solution().generate(100,ret); -
编译器的优化,不同编译器优化可能不同。
-
右值引用和移动语义。
类型分类
- C++11后,右值被划分纯右值 (pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。
- 纯右值 是指那些字面值常量 或求值结果相当于字面值 或是一个不具名的临时对象。如: 42、true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调用,或者整形 a、b,a++,a+b 等。
- 将亡值 是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达。
- 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。

引用折叠
- C++中不能直接定义引用的引用如 int& && r = i ,这样写会直接报错,通过模板 或 typedef中的类型操作可以构成引用的引用。
- 通过模板或 typedef 中的类型操作构成引用的引用时 ,C++11给出了一个引用折叠 的规则:右值引用的右值引用折叠成右值引用 ,所有其他组合均折叠成左值引用。
cpp
template<class T>
void f1(T& x)//根据引用折叠,f1实例化后总是左值引用
{
cout << "f1(T& x) " << x << endl;
}
template<class T>
void f2(T&& x)//根据引用折叠,f2实例化后可以是左值引用,也可以是右值引用
{
cout << "f2(T&& x) " << x << endl;
}
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 = 0;//右值引用再右值引用->r4为int&&
f1<int>(n); //调用f1,n为左值,不发生引用折叠,实例化为void f1(int& x)
//f1<int>(0); 不发生引用折叠,0为右值,报错
f1<int&>(n); //发生引用折叠,实例化为void f1(int& x)
f1<int&&>(n); //发生引用折叠,实例化为void f1(int& x)
f1<const int&>(n);//均发生引用折叠,实例化为void f1(const int& x)
f1<const int&>(0);
f1<const int&&>(n);
f1<const int&&>(0);
//f2<int>(n); 不发生引用折叠,n为左值,报错
f2<int>(0); //不发生引用折叠,
f2<int&>(n); //发生引用折叠,实例化为void f2(int& x)
f2<int&&>(0);//发生引用折叠,实例化为void f2(int&& x)
return 0;
}


像f2函数模板 ,T&& x参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,这种函数模板的参数叫做万能引用。
cpp
template<class T>
void Function(T&& t)
{
int a = 0;
T x = a;
//x++;
cout << &a << endl;
cout << &x << endl << endl;
}
int main()
{
Function(10);//10为右值,实例化为void Function(int&& t),T为int
int a;
Function(a); //a为左值,实例化为void Function(int& t),T为int&
Function(move(a));//move(a)为右值,实例化为void Function(int&& t),T为int
const int b = 1;
Function(b); //b为const左值,实例化为void Function(const int& t),T为const int&
Function(move(b));//move(b)为const右值,实例化为void Function(const int&& t),T为const int
return 0;
}


完美转发
- 变量表达式都是左值属性 ,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值 ,即Function函数中t的属性是左值,此时把t传递给下⼀层函数Fun,那么匹配的都是左值引用版本的Fun函数。这里想要保持t对象的属性 ,就需要使用完美转发实现。
- 完美转发forward本质是⼀个函数模板,他主要通过引用折叠的方式实现,下面示例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回。
cpp
void Fun(int& x)
{
cout << x << "左值引用" << endl;
}
void Fun(const int& x)
{
cout << x << "const左值引用" << endl;
}
void Fun(int&& x)
{
cout << x << "右值引用" << endl;
}
void Fun(const int&& x)
{
cout << x << "const右值引用" << endl;
}
template<class T>
void Function(T&& t)
{
Fun(t);
Fun(forward<T>(t));
}
int main()
{ //10为右值,t即int&&,t为右值引用类型的变量表达式,属性为左值
Function(10);//Fun(t)->Fun(int& x),Fun(forward<T>(t))->Fun(int&& x)
int a;
Function(a); //Fun(t)->Fun(int& x),Fun(forward<T>(t))->Fun(int& x)
Function(move(a));//Fun(t)->Fun(int& x),Fun(forward<T>(t))->Fun(int&& x)
const int b = 1;
Function(b); //Fun(t)->Fun(const int& x),Fun(forward<T>(t))->Fun(const int& x)
Function(move(b));//Fun(t)->Fun(const int& x),Fun(forward<T>(t))->Fun(const int&& x)
return 0;
}
万能引用在List中的使用。
cpp
//List.h
namespace mine
{
template<class T>
struct list_node
{
T _data;
list_node<T>* _next;
list_node<T>* _prev;
list_node() = default;
template<class X> //万能引用
list_node(X&& data)
:_data(forward<X>(data))
, _next(nullptr)
, _prev(nullptr)
{}
};
template<class T, class Ref, class Ptr>
struct list_iterator
{
typedef list_node<T> Node;
typedef list_iterator<T, Ref, Ptr> Self;
Node* _node;
list_iterator(Node* node)
:_node(node)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
//........................
};
template<class T>
class list
{
typedef list_node<T> Node;
public:
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
iterator begin()
{
return _head->_next;
}
iterator end()
{
return _head;
}
const_iterator begin() const
{
return _head->_next;
}
const_iterator end() const
{
return _head;
}
void empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
list()
{
empty_init();
}
//..............................
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
// 万能引用
template<class X>
void push_back(X&& x)
{
insert(end(), forward<X>(x));
}
void push_front(const T& x)
{
insert(begin(), x);
}
template<class X> //万能引用
iterator insert(iterator pos, X&& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(forward<X>(x));
// prev newnode cur
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
prev->_next = newnode;
++_size;
return newnode;
}
//........................
private:
Node* _head;
size_t _size;
};
}
可变参数模板
基本语法
-
C++11支持可变参数模板,即支持可以改变参数的数量 的函数模板和类模板,可变数量的参数 被称为参数包 ,存在两种参数包:模板参数包 ,表示0或多个模板参数;函数参数包:表示0或多个函数参数。
cpptemplate<class ...Args> void Func(Args... args) {} template<class ...Args> void Func(Args&... args) {} template<class ...Args> void Func(Args&&... args) {} -
用省略号 来指出⼀个模板参数或函数参数的⼀个包,在模板参数列表中,class...或typename...指出接下来的参数的0或多个类型列表 ;在函数参数列表中,类型名后面跟 ... 指出接下来0或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板⼀样,每个参数实例化时遵循引用折叠规则。
-
如果是 Args&&... args 的参数包,要用完美转发参数包。
cppstd::forward<Args>(args)... -
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
-
使用 sizeof... 运算符去计算参数包中参数的个数。
cpp
template<class ...Args>
void Print(Args&&... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
double x = 2.2;
Print();
Print(1);
Print(1, string("abcd"));
Print(1.1, string("abcd"), x);
return 0;
}

cpp
// 原理1:编译本质:这里会结合引用折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);
// 原理2:如果没有可变参数模板,我们需要实现出多个函数模板才能支持这里的功能。
// 有了可变参数模板,我们进一步被解放,它是类型泛化基础上叠加数量变化,让我们泛型编程更灵活。
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);
// ...
总结:
模板,一个函数模板实例化出多个 不同参数类型 的函数。
可变参数模板,一个可变参数模板函数实例化出多个 参数类型和个数都不同 的函数。
包扩展
对于⼀个参数包,除了能计算它的参数个数,还能扩展它。当扩展⼀个包时,还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。底层的实现细节如下。
cpp
void ShowList()// 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数
{
cout << endl;
}
template<class T,class ...Args>
void ShowList(T x,Args&&... args)
{
cout << x << " "; // args是N个参数的参数包,调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包
ShowList(args...);
}
template<class ...Args>// 编译时递归推导解析参数
void Print(Args... args)
{
ShowList(args...);
}
int main()
{
Print();
Print(1);
Print(1, string("abcd"));
Print(1, string("abcd"), 2.2);
return 0;
}

cpp
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)...);
cout << endl;
}
int main()
{
Print(1, string("abcdefg"), 2.2);
return 0;
}
注意,GetArg函数必须返回对象,这里的 ... 会把 args 这个参数包拆成一个个独立的实参,对每个实参调用 GetArg,再把这些调用的返回值重新打包成一个新的参数包,传给 Arguments。例如当调用 Print(1, "hello", 3.14)时,args 就是 (1, "hello", 3.14) 这个参数包。GetArg(args)... 会被展开为 GetArg(1), GetArg("hello"), GetArg(3.14)。每个 GetArg 会打印对应的参数,然后返回这个参数本身。这些返回值被重新打包成 (1, "hello", 3.14),传给 Arguments 函数。
emplace系列接口

- C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为模板可变参数,功能上兼容push和insert系列,但是empalce还支持新玩法,假设容器为container< T>,empalce还支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。
- emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列。
cpp
#include<list>
int main()
{
list<mine::string> lt;
mine::string s1("abcdefg");
mine::string s2("abcdefg");
cout << endl;
cout << "emplace_back ";
lt.emplace_back(s1);
cout << "push_back ";
lt.push_back(s2);
cout << "**************************" << endl << endl;
lt.emplace_back(move(s1));
lt.push_back(move(s2));
cout << "**************************" << endl << endl;
lt.emplace_back("abcdefg");
cout << endl;
lt.push_back("abcdefg");
return 0;
}

可以看到,传左值和传右值emplace_back和push_back是一样的,但是直接传字符串时,emplace_back因为可变参数模板将字符串"abcdefg"直接识别为const char*,所以只有一次构造;"abcdefg"是常量字符串,是右值,调用push_back的右值引用版本,所以先构造再移动构造。

cpp
int main()
{
list<pair<mine::string, int>> lt;
pair<mine::string, int> kv("sort", 1);
cout << endl;
lt.emplace_back(kv);
lt.push_back(kv);
cout << endl;
lt.emplace_back(move(kv));
lt.push_back(move(kv));
cout << endl;
lt.emplace_back("sort", 1);//会解析为void emplace_back(string&& s,int&& i)
cout << endl;
lt.push_back({ "sort",1 });//隐式类型转换
return 0;
}

在List中实现emplace 。
cpp
namespace mine
{
//........................
template <class... Args>
void emplace_back(Args&&... args)
{
insert(end(), std::forward<Args>(args)...);
}
void push_front(const T& x)
{
insert(begin(), x);
}
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
// prev newnode cur
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
prev->_next = newnode;
++_size;
return newnode;
}
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(forward<T>(x));
// prev newnode cur
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
prev->_next = newnode;
++_size;
return newnode;
}
template <class... Args>
iterator insert(iterator pos, Args&&... args)
{
Node* cur = pos._node;
Node* newnode = new Node(std::forward<Args>(args)...);
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
//........................
}
类的新功能
默认的移动构造和移动赋值
- C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
- 如果没有自己实现移动构造函数 ,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个 。那么编译器会自动生成一个默认移动构造 。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,则编译器会自动生成⼀个默认移动赋值。对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。
cpp
class Person
{
public:
Person(const char* name = "张三", int age = 20)
:_name(name)
,_age(age)
{ }
//不实现析构、拷贝构造、拷贝赋值,以此让编译器自动生成默认的移动构造和移动赋值
private:
mine::string _name;
int _age;
};
int main()
{
Person p1;
Person p2 = p1;
Person p3 = move(p1);
Person p4;
p4 = move(p2);
return 0;
}

可以看到,Person类不实现移动构造和移动赋值,也会有编译器默认生成的移动构造和移动赋值。
成员变量声明时给缺省值
成员变量声明时给的缺省值是给初始化列表用的,如果没有显示在初始化列表初始化,就会在初始化列表用声明处的缺省值初始化。
default和delete
- 假设你要使用某个默认的函数,但是因为⼀些原因这个函数没有默认生成成。如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显式指定移动构造生成。
- 如果能想要限制某些默认函数的生成 ,只需在该函数声明加上 =delete 即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
STL的变化
- 新增容器unordered_set、unordered_map、forward_list、array。
- 容器新增的右值引用和移动语义相关的push/insert/emplace系列接口,initializer_list版本的构造等。
- 范围for。
lambda
lambda表达式语法
-
lambda 表达式本质是⼀个匿名函数对象 ,跟普通函数不同的是它可以定义在函数内部 。lambda 表达式语法使用层而言没有类型,所以⼀般用auto 或者模板参数定义的对象去接收 lambda 对象。
-
lambda表达式的格式
cpp[capture-list](parameters)->return type{function body}; -
capture-list\]:**捕捉列表** ,该列表总是**出现在 lambda 函数的开始位置** ,编译器根据\[\]来判断接下来的代码是否为 lambda 函数,捕捉列表**能够捕捉上下文中的变量供 lambda 函数使用** ,捕捉列表**可以传值和传引用捕捉** 。捕捉列表为空也**不能省略**。
-
return type:返回值类型 ,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略 。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
-
{function body}:函数体 ,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
cpp
int main()
{
auto add1 = [](int x, int y)->int
{
return x + y;
};
cout << add1(1, 2) << endl;
auto func = []
{
cout << "auto func = []" << endl;
return;
};
func();
int a = 0, b = 1;
auto swap1 = [](int& x, int& y)
{
int t = x;
x = y;
y = t;
};
swap1(a, b);
cout << a << ": " << b << endl;
return 0;
}

捕捉列表
lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。3种捕捉方式如下。
-
第⼀种捕捉方式是在捕捉列表中显式地传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[x,y,&z] 表示x和y值捕捉,z引用捕捉。
cppint main() { int a = 1, b = 1; auto f1 = [a, &b] { return a + b; }; cout << f1() << endl; return 0; }
-
第⼆种捕捉方式是在捕捉列表中隐式捕捉 ,在捕捉列表写⼀个 = 表示隐式值捕捉 ,在捕捉列表写⼀个 & 表示隐式引用捕捉 ,这样 lambda 表达式中用了哪些变量,编译器就会自动捕捉哪些变量。
cppint main() { int a = 1, b = 1, c = 2; auto f2 = [=] { return a + b + c; }; cout << f2() << endl; auto f3 = [&] { a++; b++; c++; cout << "a=" << a << " " << "b=" << b << " " << "c=" << c << " " << endl; }; f3(); return 0; }
-
第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显式捕捉 。[=, &x]表示其他变量隐式值捕捉,x引用捕捉;[&, x, y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是&或= ,并且**&混合捕捉时,后面的捕捉变量必须是值捕捉**,同理 =混合捕捉时,后面的捕捉变量必须是引用捕捉。
cppint main() { int a = 1, b = 1, c = 2, d = 3; auto f4 = [&, a, b] { c++; d++; return a + b + c + d; }; cout << f4() << endl; auto f5 = [=, &a, &b] { a++; b++; return a + b + c + d; }; cout << f5() << endl; return 0; }
lambda 表达式如果在函数局部域中,它可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使用 。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
默认情况下, lambda 捕捉列表是被const修饰的 ,即传值捕捉的过来的对象不能修改 ,mutable 加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
cpp
int x = 0;
auto f = []
{
cout << x << endl;
};
int main()
{
f();
static int y = 2;
auto f1 = []
{
return x + y;
};
cout << f1() << endl;
int a = 0, b = 1, c = 2, d = 3;
auto f2 = [=]()mutable
{
a++;
b++;
c++;
d++;
return a + b + c + d;
};
cout << f2() << endl;
return 0;
}

lambda的应用
在学习 lambda 表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义⼀个类,相对会比较麻烦。使用 lambda 去定义可调用对象,既简单又方便。
cpp
struct Goods
{
string _name;
double _price;
int _evaluate;
Goods(string name="",double price=0,int evaluate=0)
:_name(name)
,_price(price)
,_evaluate(evaluate)
{ }
};
struct ComparePriceLess
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._price < g2._price;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3}, { "菠萝", 1.5, 4 } };
for (auto& e : v)
{
cout << e._name << " " << e._price << " " << e._evaluate << endl;
}
cout << endl;
//sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price;});
for (auto& e : v)
{
cout << e._name << " " << e._price << " " << e._evaluate << endl;
}
return 0;
}
lambda的原理
范围for底层是迭代器,而lambda底层是仿函数对象,即我们写了⼀个lambda 以后,编译器会生成⼀个对应的仿函数的类。
仿函数的类名是编译按⼀定规则生成的,保证不同的 lambda 生成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体 ,lambda 的捕捉列表本质是生成的仿函数类的成员变量,即捕捉列表的变量都是 lambda 类构造函数的实参。对于隐式捕捉,编译器要看使用哪些就传哪些对象。
cpp
class Rate
{
public:
Rate(double rate)
:_rate(rate)
{ }
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
double rate = 0.5;
auto f = [rate](double money, int year)
{
return money * rate * year;
};
cout << f(1000, 2) << endl;//lambda表达式
Rate r(rate);
cout << r(1000, 2) << endl;//仿函数
return 0;
}

包装器
function
-
std::function 是⼀个类模板,也是⼀个包装器,被定义在< functional>头文件中。 std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调用对象被称为 std::function 的目标。若 std::function 不含目标,则称它为空。调用空std::function 的目标导致抛出 std::bad_function_call 异常。
cpptemplate<class Ret,class... Args> class function<Ret(Args...)>; -
函数指针、仿函数、 lambda 等可调用对象的类型各不相同, std::function 的优势就是统一类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型。
cpp
#include<functional>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
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;
return 0;
}

cpp
class Plus
{
public:
Plus(int n=1)
:_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)> f4 = &Plus::plusi;
cout << f4(1, 1) << endl;
//包装普通成员函数
//普通成员函数还有一个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以
Plus pd;
function<double(Plus*, double, double)>f5 = &Plus::plusd;
cout << f5(&pd,1.1, 1.1) << endl;
function<double(Plus, double, double)>f6 = &Plus::plusd;
cout << f6(pd, 1.1, 1.1) << endl;
function<double(Plus&&, double, double)>f7 = &Plus::plusd;
cout << f7(move(pd), 1.1, 1.1) << endl;
cout << f7(Plus(), 1.1, 1.1) << endl;
return 0;
}

使用map,让string映射function完成逆波兰表达式求值,这种方式的最大优势之⼀是方便扩展,假设还有其他运算,我们增加map中的映射即可。

cpp
class Solution {
public:
int evalRPN(vector<string>& tokens)
{
stack<int> st;
map<string, function<int(int, int)>> m = {
{"+",[](int a,int b) {return a + b;}},
{"-",[](int a,int b) {return a - b;}},
{"*",[](int a,int b) {return a * b;}},
{"/",[](int a,int b) {return a / b;}},
};
for (auto& e : tokens)
{
if (m.count(e))//遇到运算符就运算
{
int right = st.top();//由于栈后进先出,所以栈顶元素是运算符的右操作数
st.pop();
int left = st.top();
st.pop();
int ret = m[e](left, right);
st.push(ret);
}
else
st.push(stoi(e));
}
return st.top();
}
};

bind

-
bind 是⼀个函数模板 ,它也是⼀个可调用对象的包装器,可以把它看做⼀个函数适配器,对接收的fn可调用对象进行处理后返回一个可调用对象 。bind 可以用来调整参数个数和参数顺序。bind 也在< functional>这个头文件中。
-
调用bind的一般形式:
cppauto newCallable = bind(callable,arg_list);newCallable本身是⼀个可调用对象,arg_list是⼀个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
-
arg_list 中的参数可能包含形如 _n 的名字,其中n是⼀个整数,这些参数是占位符 ,表示newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表示生成的可调用对象中参数的位置: _1为newCallable的第⼀个参数, _2为第⼆个参数,以此类推。 _1/ _2/ _3......这些占位符放到placeholders的⼀个命名空间中。
cpp
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int Sub(int a, int b)
{
return (a - b) * 10;
}
int SubX(int a, int b,int c)
{
return (a - b - c) * 10;
}
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
auto sub1 = bind(Sub, _1, _2);
cout << sub1(10, 5) << endl;
//bind本质返回一个仿函数对象
//调整参数顺序(不常用)
auto sub2 = bind(Sub, _2, _1);
cout << sub2(10, 5) << endl;
return 0;
}

cpp
int main()
{
//调整参数个数(常用)
auto sub3 = bind(Sub, 100, _1);
cout << sub3(20) << endl;
auto sub4 = bind(Sub, _1, 100);
cout << sub4(20) << endl;
auto sub5 = bind(SubX, 100, _1, _2);
cout << sub5(50, 10) << endl;
auto sub6 = bind(SubX, _1, 100, _2);
cout << sub6(50, 10) << endl;
auto sub7 = bind(SubX, _1, _2, 100);
cout << sub7(50, 10) << endl;
return 0;
}

cpp
int main()
{
//bind一般用于绑死一些固定参数
Plus pd;
function<double(Plus&&, double, double)> f = &Plus::plusd;
cout << f(move(pd), 1.1, 2.2) << endl;
cout << f(Plus(), 1.2, 1.3) << endl;
//绑死成员函数对象,就不需要每次都传递了
function<double(double, double)> fb = bind(&Plus::plusd, Plus(), _1, _2);
cout << fb(1.1, 2.2) << endl;
cout << fb(1.2, 1.3) << endl;
return 0;
}

cpp
int main()
{ //计算复利
auto func = [](double money, double rate, int year)
{
double ret = money;
for (int i = 0;i < year;i++)
{
ret += ret * rate;
}
return ret - money;
};
function<double(double)> f3_1_5 = bind(func, _1, 0.015, 3);//bind绑死了rate和year,()里只需要money的类型
function<double(double)> f5_1_5 = bind(func, _1, 0.015, 5);
function<double(double)> f3_2_5 = bind(func, _1, 0.025, 3);
function<double(double)> f5_2_5 = bind(func, _1, 0.025, 5);
cout << f3_1_5(10000) << " ¥" << endl;
cout << f5_1_5(20000) << " ¥" << endl;
cout << f3_2_5(30000) << " ¥" << endl;
cout << f5_2_5(40000) << " ¥" << endl;
return 0;
}
