欢迎光临
我们一直在努力

【C++11】C++11核心特性全解析,一文看懂现代C++的起点

目录

1.C++11发展史

2.列表初始化{}

2.1 C++98的{}

2.2 C++11的{}

2.2.1 std::initializer_list

3.右值引用和移动语义

3.1 左值和右值

3.2.1 类型分类

3.2 左值引用和右值引用

3.3 引用延长生命周期

3.4 左值和右值的参数匹配

3.5 右值引用和移动语义搭配使用的场景

3.5.1 移动构造和移动赋值

3.5.2 右值引用和移动语义解决传返回值问题

3.5.3 右值引用和移动语义的提效

3.6 引用折叠

3.7 完美转发

4.可变参数模板

4.1 基本语法

4.2 包扩展

4.3 emplace系列接口

5.新的类功能

5.1 默认移动构造和移动赋值

5.2 default 和 delete

6.lambda

6.1 lambda表达式语法

6.2 捕捉列表

6.3 lambda的原理

6.4 lambda的使用场景

7.包装器

7.1 function

7.2bind


1.C++11发展史

C++11 是 C++ 历史上最具颠覆性的版本,标志着“现代 C++”的开端。它的诞生结束了长达 13 年的标准停滞,核心目标是将 C++ 从一门偏底层的“系统级语言”,升级为更安全、更简洁、更高效的通用语言。

2.列表初始化{}

2.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.2 C++11的{}

        C++11以后想统⼀初始化方式,试图实现⼀切对象皆可用{}初始化,{}初始化也叫做列表初始化。它支持内置类型,也支持自定义类型,自定义类型的本质是类型转换,中间会产生临时变量,但优化后为直接构造。

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 x = { 1 };
int y{ 1 }; // = 可以省略

Date d1 = { 2026,1,1 }; //本质是类型转换:临时对象+拷贝构造->优化为直接构造 (类型转换)
Date d2{ 2026,7,13 };
Date d3{ 2026 };
Date d4 = 2027; //单参数支持的类型转换也可以不用加{}

vector<Date> v;
v.push_back(d1);
v.push_back(Date(2026, 7, 12));
v.push_back({ 2026,7,15 }); //比起有名对象和匿名对象传参,{}更具性价比

const Date& d5 = { 2026,7,14 }; //这里d5引用的是{2026,7,14} 构造的临时对象,因为临时对象作为纯右值,要用const来接收。
return 0;
}

2.2.1 std::initializer_list

        C++11库中提出了⼀个std::initializer_list的类, auto il = { 10, 20, 30 }; // the type of il is an initializer_list ,这个类的本质是底层开⼀个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。容器有了initializer_list,就可以通过initializer_list实现的构造函数支持一次性构建任意多个值,{x1,x2,x3…}。

// 另外,容器的赋值也⽀持initializer_list的版本
vector& operator= (initializer_list<value_type> il);
map& operator= (initializer_list<value_type> il);

int main()
{
initializer_list<int> mylist;
mylist = { 10,20,30 };

// 这⾥begin和end返回值是initializer_list对象中存的两个指针
// 这两个指针的值跟i的地址跟接近,说明数组存在栈上
int i = 0;
cout << mylist.begin() << endl;
cout << mylist.end() << endl;
cout << &i << endl;

// {}列表中可以有任意多个值
// 这两个写法语义上还是有差别的,第⼀个v1是直接构造,
// 第⼆个v2是构造临时对象+临时对象拷⻉v2+优化为直接构造
// 注意:这里的{}和前面的列表初始化还不太一样,上面的是类型转换,这里走的是initializer_list实现的构造函数
vector<int> v1({ 1,2,3,4,5 });
vector<int> v2 = { 1,2,3,4,5 };
const vector<int>& v3 = { 1,2,3,4,5 };
return 0;
}

3.右值引用和移动语义

        C++98也有引用的说法,不过那是左值引用,C++11新增了右值引用的语法特性,无论是左值引用还是右值引用,都是给对象取别名。

3.1 左值和右值

        左值:左值是⼀个数据表达式,⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左右两边。定义时用const修饰符后的左值,不能给他赋值,但是可以取它的地址。

        右值:右值也是⼀个数据表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现在左边,右值不能取地址。

        也就是说左值和右值的核心区别是能否取地址。

int main()
{
// 左值:可以取地址
// 以下的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; //加(void*)强转,避免C字符串

// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值
10;
x + y;
fmin(x, y);
string("11111");
return 0;
}

3.2.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)。         泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。

3.2 左值引用和右值引用

        Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引用,第⼆个就是右值引用。左值引用不能直接引用右值,但是const左值引用可以引用右值,右值引用不能直接引用左值,但是右值引用可以引用move(左值)。

        move是库里面的⼀个函数模板,本质内部是进行强制类型转换,当然他还涉及⼀些引用折叠的知识,下面就会讲到。

        需要注意的是变量表达式都是左值属性。也就意味着⼀个右值被右值引用绑定后,右值引用变量的变量表达式的属性实际是左值。

// 左值引⽤给左值取别名
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("11111");

// 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");

// 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
string&& rrx5 = (string&&)s;

//rrx1、rrx2…. rr1、rr2…都是左值属性,需要再move一下
int&& rrx6 = move(rr1);

        语法层面看,左值引用和右值引用都是取别名,不开空间。但从汇编底层的角度看上面代码中r1和rr1汇编层实现,底层都是用指针实现的,没什么区别。

3.3 引用延长生命周期

        右值引用可用于为临时对象延长生命周期且可以修改,const 的左值引用也能延长临时对象生存期,但这些对象无法被修改。

int main()
{
string s1 = "Test";

//const 的左值引⽤延⻓⽣存期
const string& r2 = s1 + s1;

// r2 += "Test"; // 错误:不能通过到 const 的引⽤修改

// 右值引⽤延⻓⽣存期
string&& r3 = s1 + s1;

r3 += "Test";//能通过到⾮ const 的引⽤修改
cout << r3 << '\\n';
return 0;
}

3.4 左值和右值的参数匹配

        C++98中,我们实现⼀个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会匹配f(左值引用),实参是const左值会匹配f(const 左值引用),实参是右值会匹配f(右值引用)。

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;
int&& x = 1;

f(i); // 调⽤ f(int&)
f(ci); // 调⽤ f(const int&)
f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&)
f(std::move(i)); // 调⽤ f(int&&)
f(x);// 调⽤ f(int& x)
f(std::move(x)); // 调⽤ f(int&& x)

return 0;
}

3.5 右值引用和移动语义搭配使用的场景

        回顾之前左值引用的使用场景:

        左值引用主要用于函数中引用传参和引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝的效率问题,但是有些场景不能使用传左值引用返回,如下面的两个函数。

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; //str出了函数作用域就销毁了,传引用返回会造成野引用
}

// 这⾥的传值返回拷⻉代价又太⼤了
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;
}
};

        这个时候无论是左值引用还是右值引用都不能解决问题,因为函数体里声明定义的变量只要出了这个函数作用域就会销毁,这个时候就要考虑其他解决方法了。

3.5.1 移动构造和移动赋值

        移动构造函数也是⼀种构造函数,类似拷贝构造函数,但移动构造函数要求第⼀个参数是该类类型的右值引用,根拷贝构造要求一样,如果还有其他参数,额外的参数必须有缺省值。         移动赋值函数跟拷贝赋值构成函数重载,移动赋值函数也要求第⼀个参数是该类类型的右值引用。         对于像string/vector这样的需要深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为他们的本质是要“窃取”右值引用对象的资源,而不是去拷贝资源,从提高效率。下面的A::string样例实现了移动构造和移动赋值,我们分析一下。

        只需在我们之前原本模拟实现的string基础上再手动实现一下移动构造和移动赋值即可。

string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)-构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}

string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) — 拷⻉构造" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
// 移动构造
string(string&& s)
{
cout << "string(string&& s) — 移动构造" << endl;
swap(s);
}
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 ch : s)
{
push_back(ch);
}
}
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) — 移动赋值" << endl;
swap(s);
return *this;
}

void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}

~string()
{
cout << "~string() — 析构" << endl;
delete[] _str;
_str = nullptr;
}

        打印结果:

3.5.2 右值引用和移动语义解决传返回值问题

namespace A
{
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; //str出了函数作用域就销毁了,传引用返回会造成野引用
}
}

//场景一:
int main()
{
A::string ret = A::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}

//场景二:
int main()
{
A::string ret;
ret = A::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}

        这里没有调用拷贝构造,也没有调用移动构造,是因为编译器优化的太厉害了,这str的本质就是ret的引用,底层通过指针实现,通过打印地址可以发现是一样的。关闭编译器的优化的话,这里应该会调用移动构造。

3.5.3 右值引用和移动语义的提效

        当实参是⼀个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象;当实参是⼀个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上

        通过下面的代码可以感受一下:

namespace A
{
template<class T>
class list
{
typedef ListNode<T> Node;
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
void empty_init()
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
}
list()
{
empty_init();
}
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& x)
{
insert(end(), move(x));
}
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* newnode = new Node(x);
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* newnode = new Node(move(x));
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
private:
Node* _head;
};
}

int main()
{
A::list<A::string> lt;
cout << "*************************" << endl;

A::string s1("111111111111111111111");
lt.push_back(s1);
cout << "*************************" << endl;

lt.push_back(A::string("22222222222222222222222222222"));
cout << "*************************" << endl;

lt.push_back("3333333333333333333333333333");
cout << "*************************" << endl;

lt.push_back(move(s1));
cout << "*************************" << endl;
return 0;
}

        打印结果:

3.6 引用折叠

        C++中不能直接定义引用的引用,通过模板或 typedef中的类型操作可以构成引用的引用。         通过模板或 typedef 中的类型操作可以构成引用的引用的同时,C++11给出了⼀个引用折叠的规则:右值引用的右值引用折叠成右值引用,其他组合均折叠成左值引用。

int&& & r1 = 5; //报错,不能直接这样写

// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{
}
// 由于引⽤折叠限定,传左值f2实例化就是左值引用,传右值就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;
}

        Function(T&& t)函数模板程序中,实参是int右值,模板参数T的推导就是int,实参是int左值,模板参数T的推导就是int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的Function,实参是右值,实例化出右值引用版本形参的Function,这种函数模板参数也叫做万能引用。

//万能引用
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;
// b是左值,推导出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));

return 0;
}

3.7 完美转发

        Function(T&& t)函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化以后是右值引用的Function函数。我们已经知道变量表达式都是左值属性,也就是说Function中的t始终是左值属性,如果我们要将t传给下一个函数Func是,始终只会匹配左值引用的版本,这里我们要保持t的属性的话,就需要用到完美转发。

template <class T> T&& forward (typename remove_reference<T>::type&arg);

template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{
// forward an lvalue as either an lvalue or an rvalue
return static_cast<_Ty&&>(_Arg); //这里的static_cast 可以理解为强制类型转换
}

        完美转发forward本质是⼀个函数模板,他主要还是通过引用折叠的方式实现,传递Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回。

template<class T>
void Function(T&& t)
{
Fun(forward<T>(t));
}

4.可变参数模板

4.1 基本语法

        C++98中模板实现的功能是参数类型可变,而可变参数模板在其的基础上还实现了模板参数数量可变,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。

template <class …Args> void Func(Args… args) {}
template <class …Args> void Func(Args&… args) {}
template <class …Args> void Func(Args&&… args) {}

        我们用省略号来指出⼀个模板参数或函数参数的表示⼀个包,在模板参数列表中,class…或 typename…指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟…指出 接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,且每个参数实例化时也要遵循引用折叠规则。可变参数模板的本质还是去实例化对应类型和个数的多个函数。

        有了可变参数模板,我们进⼀步被解放,他是类型泛化基础上叠加数量变化,让我们泛型编程更灵活。

        sizeof可以计算参数包中参数的个数。

template <class …Args>
void Print(Args&&… args)
{
cout << sizeof…(args) << endl;
}
int main()
{
double x = 2.2;
Print();// 包⾥有0个参数
Print(1);// 包⾥有1个参数
Print(1, string("xxxxx"));// 包⾥有2个参数
Print(1.1, string("xxxxx"), x);// 包⾥有3个参数
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);

4.2 包扩展

        对于⼀个参数包,除了能计算他的参数个数,我们还能做的唯⼀的事情就是扩展它,通过在模式的右边放⼀个省略号(…)来触发扩展操作。

递归扩展:

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(1, string("xxxxx"), 2.2);
return 0;
}

或:

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)…);
}
// 本质可以理解为编译器编译时,包的扩展模式
// 将上⾯的函数模板扩展实例化为下⾯的函数
//void Print(int x, string y, double z)
//{
//Arguments(GetArg(x), GetArg(y), GetArg(z));
//}

int main()
{
Print(1, string("xxxxx"), 2.2);
return 0;
}

折叠表达式:(C++17)

// 折叠表达式 C++17
template <class …Args>
void Print(Args… args)
{
((cout << args << " "), …);

cout << "\\n";
}

4.3 emplace系列接口

        C++11之后新增了emplace系列的接口,接口均为模板可变参数,功能上兼容push和insert系列,但是empalce还支持新玩法,假设容器为container<T>,empalce还支持直接插入构造T对象的参数,这样在有些场景会更高效,可以直接在容器空间上构造T对象。推荐用emplace系列替代insert和push系列。

template <class… Args> void emplace_back (Args&&… args);
template <class… Args> iterator emplace (const_iterator position,Args&&… args);

        在原本模拟实现list的基础上,我们补充实现emplace和enplace_back接口,这里把参数包不断的向下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,达到了前面说的empalce支持直接插入构造T对象的参数。

iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* newnode = new Node(x);
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* newnode = new Node(move(x));
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}

template <class… Args>
void emplace_back(Args&&… args)
{
insert(end(), std::forward<Args>(args)…);
}

// 原理:本质是编译器根据可变参数模板⽣成对应参数的函数
//void emplace_back(string& s)
//{
//insert(end(), std::forward<string>(s));
//}
//void emplace_back(string&& s)
//{
//insert(end(), std::forward<string>(s));
//}
//void emplace_back(const char* s)
//{
//insert(end(), std::forward<const char*>(s));
//}

int main()
{
list<A::string> lt;

// 传左值,跟push_back一样,走拷贝构造
//string s1("111111111111");
//lt.emplace_back(s1);
//cout << "*********************************" << endl;

//// 右值,跟push_back一样,走移动构造
//lt.emplace_back(move(s1));
//cout << "*********************************" << endl;

// 直接把构造string参数包往下传,直接用string参数包构造string
// 这里达到的效果是push_back做不到的
lt.push_back("111111111111"); //构造+移动构造
cout << "*********************************" << endl;

lt.emplace_back("111111111111"); // 直接构造
cout << "*********************************" << endl;

*****************************************************************
A::list<Date> lt;
// 构造 + 拷贝构造
Date d1{ 2025,11,18 };
lt.push_back(d1);
lt.push_back({ 2025,11,18 });

// 传构造Date的参数,传给形参参数包,参数包往下不断传递,最后直接构造到链表节点上
// 直接构造
lt.emplace_back(2025,11,18);

return 0;
}

5.新的类功能

5.1 默认移动构造和移动赋值

        在原C++类中,一共有六个默认成员函数,这里新增了默认移动构造和默认移动赋值。

        如果你没有自己实现移动构造/移动赋值函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意⼀个。那么编译器会自动生成⼀个默认移动构造/默认移动赋值。默认生成的移动构造/移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝(浅拷贝),自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

5.2 default 和 delete

        如果你提供了拷贝构造函数,但还是想使用移动构造,这时编译器是不会自动生成的,那么我们就可以使用default关键字来显示指定移动构造生成。所以假设你要使用某个默认的函数,但是因为⼀些原因这个函数没有默认生成,就可以用default。delete就更简单了,就是限制某些默认函数的生成。在C++98中我们想要到达这样的效果只能进行声明但不定义,且放在private里面,避免其他地方定义。

        使用如下:

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;
//Person(const Person& p) = delete;
private:
A::string _name;
int _age;
};

6.lambda

        lambda的用法非常广泛,一定程度上美化并简洁了写法,后面会逐渐感受到。

6.1 lambda表达式语法

        lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。语法层面而言它没有类型,一般我们都是通过auto或者模板参数定义的对象去接收lanbda对象。

        lambda表达式格式:

[capture-list] (parameters)-> return type {function boby }

        这里的返回值是后置的,如果返回值特别长,不那么容易一眼看到函数名和参数的话就可以采取这种后置的写法,对于普通函数而言也是可以支持的。

        [capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量来供 lambda 函数使用,捕捉列表可以传值和传引用捕捉,且捕捉列表为空也不能省略。

        (parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略。

        ->return type :返回值类型,没有返回值时此部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。

      {function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以 使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。  

int main()
{
//一个简单的lambda表达式
auto add1 = [](int x, int y)->int {return x + y; };
cout << add1(1, 2) << endl;

// 1、捕捉为空也不能省略
// 2、参数为空可以省略
// 3、返回值可以省略
// 4、函数体不能省略
auto func1 = []
{
cout << "hello world" << endl;
};
func1();

int a = 0, b = 1;
auto swap1 = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
swap1(a, b);
cout << a <<" " << b << endl; //1 0

return 0;
}

6.2 捕捉列表

        lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。

        第⼀种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉。[x,y, &z] 表示x和y值捕捉,z引用捕捉。

        第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表示隐式值捕捉,在捕捉列表写⼀个&表示隐式引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量。

        第三种就是混合捕捉,[=, &x]表示其他变量隐式值捕捉,x引用捕捉;[&, x, y]表示其他变量引用捕捉,x和y值捕捉。

        lambda的捕捉列表不能捕捉静态局部变量和全局变量,因为他们也不选要捕捉,lambda表达式中可以直接使用,也就是说,如果lambda表达式定义在全局,捕捉列表必须为空。

        注意:这里的值捕捉实际上是外面变量对象的一份拷贝,默认是被const修饰,不能修改的,想要修改的的话可以在参数列表的后面加上mutable,引用捕捉可以修改。等下面讲lambda底层的时候就能理解了。

int x = 0;
//全局变量不能捕捉也不需要捕捉
auto func1 = []()
{
x++;
};

class B
{
public:
void func()
{
int x = 0, y = 1;
//this指针也是可以捕获的,且可修改,因为是通过指针访问的
auto f1 = [=]
{
_a1++;
return x + y + _a1 + _a2;
};

cout << f1() << endl;

auto f2 = [&]
{
x++;
_a1++;
return x + y + _a1 + _a2;
};

cout << f2() << endl;

// 捕捉this本质是可以访问成员变量
auto f3 = [x, this]
{
_a1++;
return x + _a1 + _a2;
};

cout << f3() << endl;
}

private:
int _a1 = 0;
int _a2 = 1;
};

int main()
{
// 只能用当前lambda局部域捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3;

//auto func1 = [a, &b] () mutable
auto func1 = [a, &b]
{
// 值捕捉的变量不能修改,引用捕捉的变量可以修改
// a++;
b++;
int ret = a + b;
x++;
return ret;
};
cout << func1() << endl;

// 隐式值捕捉
auto func2 = [=]
{
int ret = a + b + c;
return ret;
};
cout << func2() << endl;

// 隐式引用捕捉
auto func3 = [&]
{
a++;
c++;
d++;
};
func3();
cout << a << " " << b << " " << c << " " << d << endl;

// 混合捕捉1
auto func4 = [&, a, b]
{
//a++;
//b++;
c++;
d++;
return a + b + c + d;
};
func4();
cout << a << " " << b << " " << c << " " << d << endl;

return 0;
}

6.3 lambda的原理

        lambda底层是仿函数对象,也就说我们写了⼀个lambda 以后,编译器会生成⼀个对应的仿函数的类。

        仿函数的类名是编译按⼀定规则生成的,保证不同的 lambda ⽣成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参。

auto func5 = [a, &b](int x)
{
++b;
return a + b + x;
};
func5(1);
//当我们使用func5这个对象时,编译器会生成对应一个类并实现仿函数。
//等价于 lambda5 func5(a, b);

class lambda5
{
public:
lambda5(int a_, int& b_)
:a(a_)
, b(b_)
{}

int operator()(int x)
{
++b;
return a + b + x;
}
private:
const int a; //加了mutable 就相当于去掉了const
int& b;
};

6.4 lambda的使用场景

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;
}
};

struct CompareEvaluateLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._evaluate < gr._evaluate;
}
};

struct CompareEvaluateGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._evaluate > gr._evaluate;
}
};

int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3}, { "菠萝", 1.5, 4 } };

// 类似这样的场景,我们实现仿函数对象或者函数指针支持商品中不同项的比较
//相对还是比较麻烦的,但是这里lambda就很好用了
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
sort(v.begin(), v.end(), CompareEvaluateLess());
sort(v.begin(), v.end(), CompareEvaluateGreater());

return 0;
}

        以上场景用lambda就会变得很简洁。

sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) {
return gl._price < gr._price;
});//模板参数推导

sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) {
return gl._price > gr._price;
});

sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) {
return gl._evaluate < gr._evaluate;
});

sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) {
return gl._evaluate > gr._evaluate;
});

        排价格的升序:

7.包装器

7.1 function

template <class T>
class function; // undefined

template <class Ret, class… Args>
class function<Ret(Args…)>;

        std::function 是⼀个类模板,也是⼀个包装器。 std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调用对象被称为 std::function 的目标。若 std::function 不含目标,则称它为空。调用空std::function 的目标导致抛出 std::bad_function_call 异常。

        使用时需要包含头文件<functional>,Ret表示返回类型,Args表示参数包中的参数类型和个数。        

#include<functional>
int f(int a, int b)
{
return a + b;
}

struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};

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;

vector<function<int(int, int)>> v;
v.push_back(f);
v.push_back(Functor());
v.push_back([](int a, int b) {return a + b; });

for (auto& f : v)
{
cout << f(1, 1) << endl;
}

function<int(int, int)> f4 = &Plus::plusi;
cout << f4(1, 1) << endl;

//普通成员函数有this指针,需要传类/类对象或者类/类对象的指针
function<double(Plus*, double, double)> f5 = &Plus::plusd;
Plus ps;
cout << f5(&ps, 1.1, 1.1) << endl;

function<double(Plus, double, double)> f6 = &Plus::plusd;
cout << f6(ps, 1.1, 1.1) << endl;

function<double(Plus, double, double)> f7 = &Plus::plusd;
cout << f7(Plus(), 1.1, 1.1) << endl;

function<double(Plus&&, double, double)> f8 = &Plus::plusd;
cout << f8(Plus(), 1.1, 1.1) << endl;

auto pf1 = &Plus::plusd;
Plus* ptr = &ps;
cout << (ps.*pf1)(1.1, 1.1) << endl;
cout << (ptr->*pf1)(1.1, 1.1) << endl;

return 0;
}

逆波兰表达式的lambda写法     逆波兰表达式

        

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();
}
};

7.2bind

simple(1)
template <class Fn, class… Args>
/* unspecified */ bind (Fn&& fn, Args&&… args);

with return type (2)
template <class Ret, class Fn, class… Args>
/* unspecified */ bind (Fn&& fn, Args&&… args);

        bind 是⼀个函数模板,它也是⼀个可调用对象的包装器,可以把他看做⼀个函数适配器,对接收的fn可调用对象进行处理后返回⼀个可调用对象。 bind 可以用来调整参数个数和参数顺序。

        调用bind的⼀般形式: auto newCallable = bind(callable,arg_list); 其中newCallable本身是⼀个可调用对象,arg_list是⼀个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。

        arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,数值n表示生成的可调用对象中参数的位置:_1为newCallable的第⼀个参数,_2为第⼆个参数,以此类推。_1/_2/_3….这些占位符放到placeholders的⼀个命名空间中。

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:
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()
{
// bind 本质返回的一个仿函数对象
// 调整参数顺序(不常用)
// _1代表第一个实参
// _2代表第二个实参
// …
auto f1 = bind(Sub, _1, _2);
auto f2 = bind(Sub, _2, _1);

cout << f1(10, 5) << endl;
cout << f2(10, 5) << endl;

// 调整参数个数
auto f3 = bind(SubX, 10, _1, _2); // 10被绑定
cout << f3(15, 5) << endl;
// 底层operator(),调用SubX,第一个参数10,15, 5

auto f4 = bind(SubX, _1, 10, _2);
cout << f4(15, 5) << endl;
// 底层operator(),调用SubX,第一个参数15,10, 5

auto f5 = bind(SubX, _1, _2, 10);
cout << f5(15, 5) << endl;
// 底层operator(),调用SubX,第一个参数15,5, 10

//function和bind结合使用
function<double(Plus, double, double)> f7 = &Plus::plusd;
cout << f7(Plus(), 1.1, 1.1) << endl;
cout << f7(Plus(), 2.2, 1.1) << endl;
cout << f7(Plus(), 3.3, 1.1) << endl;

function<double(double, double)> f8 = bind(&Plus::plusd,Plus(),_1,_2);
cout << f8(1.1, 1.1) << endl;
cout << f8(2.2, 1.1) << endl;
cout << f8(3.3, 1.1) << endl << endl;

// 计算复利的lambda
auto func1 = [](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)> func_r1_5_y3 = bind(func1, 0.015, _1, 3);
function<double(double)> func_r1_5_y5 = bind(func1, 0.015, _1, 5);
function<double(double)> func_r1_5_y20 = bind(func1, 0.015, _1, 20);

cout << func_r1_5_y3(100000) << endl;
cout << func_r1_5_y5(100000) << endl;
cout << func_r1_5_y20(100000) << endl;

return 0;
}

完~

赞(0)
未经允许不得转载:171主机测评 » 【C++11】C++11核心特性全解析,一文看懂现代C++的起点
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址