C++11新特性全面解析(一)

目录

1.C++11简介

2.列表初始化

2.1 c++98中{}的初始化问题

2.2 内置类型的列表初始化

2.3 自定义类型的列表初始化

3 变量类型推导

3.1 为什么需要类型推导

3.2 decltype类型推导

4.范围for

5.final与override

6.默认成员函数的控制

7.新增加容器--静态数组array、forward_list、unordered系列

8.右值引用(重点)

8.1概念

8.2.左值与右值

8.3 纯右值和将亡值

8.4 引用与右值引用比较

8.5 值的形式返回对象的缺陷

8.6 移动语义

8.7 右值引用左值

8.8 完美转发

8.9 vector新增的emplace_back究竟有何神奇之处

9. lambda表达式

9.1 c++98中的一个例子

9.2 lambda表达式

9.3 lambda表达式语法

9.4 函数对象与lambda表达式


1.C++11简介

2003 C++ 标准委员会曾经提交了一份技术勘误表 ( 简称 TC1) ,使得 C++03 这个名字已经取代了 C++98 称为C++11之前的最新 C++ 标准名称。不过由于 TC1 主要是对 C++98 标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03 标准。从 C++0x C++11 C++ 标准 10 年磨一剑,第二个真正意义上的标准珊珊来迟。相比于 C++98/03 C++11 则带来了数量可观的变化,其中包含了约 140 个新特性,以及对 C++03 标准中约 600 个缺陷的修正,这使得 C++11 更像是从 C++98/03 中孕育出的一种新语 。相比较而言, C++11 能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅 功能更强大,而且能提升程序员的开发效率

2.列表初始化

2.1 c++98中{}的初始化问题

在c++98中,标准运行使用花括号{}对数组元素进行统一的列表初始值设定,比如

int array1[] = {1,2,3,4,5};
int array2[5] = {0};

对于一些自定义的类型,却无法使用这样的初始化,比如:

vector<int> v{1,2,3,4,5};

就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。c++11扩大了用大括号括起来的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义类型,使用初始化列表时,可添加=,也可不添加

2.2 内置类型的列表初始化

int main()
 { 
 // 内置类型变量
 int x1 = {10};
 int x2{10};
 int x3 = 1+2;
 int x4 = {1+2};
 int x5{1+2};
 // 数组
 int arr1[5] {1,2,3,4,5};
 int arr2[]{1,2,3,4,5};
 
 // 动态数组,在C++98中不支持
 int* arr3 = new int[5]{1,2,3,4,5};
  return 0;}

注意:列表初始化可以在{}之前使用=,其效果与不使用=没有什么区别

2.3 自定义类型的列表初始化

1.标准库支持单个对象的列表初始化


class Point
{
public:
 Point(int x = 0, int y = 0): _x(x), _y(y)
 {}
private:
 int _x;
 int _y;
};
int main()
{
 // 标准容器
 vector<int> v{1,2,3,4,5};
 map<int, int> m{{1,1}, {2,2,},{3,3},{4,4}};
 Pointer p{ 1, 2 };
 return 0;
}

2.多个对象的列表初始化

如果我们自己实现一个类,比如类似vector之类的,像传多少就传多少

这时候就需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()end()迭代器以及获取区间中元素个数的方法size()

#include <initializer_list>
template<class T>
class Vector {
public:
 // ... 
 Vector(initializer_list<T> l): _capacity(l.size()), _size(0)
 {
 _array = new T[_capacity];
 for(auto e : l)
 _array[_size++] = e;
 }
 
 Vector<T>& operator=(initializer_list<T> l) {
 delete[] _array;
 size_t i = 0;
 for (auto e : l)
 _array[i++] = e;
 return *this;
 } 
 // ...
private:
 T* _array;
 size_t _capacity;
 size_t _size;
}

因为你不知道想要传多少个,所以c++11提供了一个类,你只需要重载一下你的构造函数,如果使用了初始化列表就会去调用这个构造函数

3 变量类型推导

3.1 为什么需要类型推导

因为有时候在定义一个变量的时候,不清楚或者变量的类型太长太复杂,就可以使用auto去帮我们自动推导类型

// 使用迭代器遍历容器, 迭代器类型太繁琐
 std::map<std::string, std::string>::iterator it = m.begin();
 while(it != m.end())
 {
 cout<<it->first<<" "<<it->second<<endl;
 ++it;
 }

3.2 decltype类型推导

auto使用的前提是必须要对auto声明的类型进行初始化,比如这里的m.begin就是一个迭代器类型,但有时候可能需要根据表达式完成之后的结果进行推导,因为编译期间,代码不会运行,此时auto也就无能为力了

decltype是编译期间进行推演,代码不会运行,仅仅进行推演类型,比如下面的a+b不会执行a+b

typeid是用来查看某个变量的类型的,不能用其返回结果来定义变量

dynamic_cast只能应用于含有虚函数的继承体系中

1.推演表达式类型作为变量的定义类型

int main()
{
 int a = 10;
 int b = 20;
 
 // 用decltype推演a+b的实际类型,作为定义c的类型
 decltype(a+b) c;
 cout<<typeid(c).name()<<endl;
 return 0;
}

2.推演函数返回值的类型

void* GetMemory(size_t size)
{
 return malloc(size);
}
int main()
{
 // 如果没有带参数,推导函数的类型
 cout << typeid(decltype(GetMemory)).name() << endl;
 
 // 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
 cout << typeid(decltype(GetMemory(0))).name() <<endl;
 
 return 0;
}

4.范围for

https://blog.csdn.net/Laydya/article/details/145682543

5.final与override

提供严格检查是否完成重写的

https://blog.csdn.net/Laydya/article/details/148176755

6.默认成员函数的控制

default:指示编译器生成默认构造函数等,关于类的默认函数

delete:指示编译器删除某个关于类的默认函数

class A
{
public:
 A(int a): _a(a)
 {}
 // 显式缺省构造函数,由编译器生成
 A() = default;
 
 // 在类中声明,在类外定义时让编译器生成默认赋值运算符重载
 A& operator=(const A& a);
private:
 int _a;
};
A& A::operator=(const A& a) = default;
int main()
{
 A a1(10);
 A a2;
 a2 = a1;
 return 0;
}



class A
{
public:
 A(int a): _a(a)
 {}
 
 // 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
 A(const A&) = delete;
 A& operator(const A&) = delete;
private:
 int _a;
};
int main()
{
 A a1(10);
 // 编译失败,因为该类没有拷贝构造函数
// A a2(a1);
// 编译失败,因为该类没有赋值运算符重载
 A a3(20);
 a3 = a2;
 return 0;
}

在c++98当中,把构造函数等直接设置成私有也行,c++11更简单,直接后面=delete

注意:避免删除函数和explicit一起使用,explicit 是为了避免意外的隐式转换导致逻辑错误

这个explicit只能修饰构造函数

7.新增加容器--静态数组array、forward_list、unordered系列

array:静态数组

forward_list:单向不带头链表

unordered系列在专栏当中已经重点讲解,可配合数据结构了解底层结构

8.右值引用(重点)

8.1概念

c++98提出了引用的概念,引用即别名,引用变量与其引用实体公用同一块内存空间,而引用的底层是通过指针来实现的,使用引用可以提高程序的可读性

https://blog.csdn.net/Laydya/article/details/145669638

为了提高程序的运行效率,c++11中引入了右值引用,右值引用也是别名,但其只能对右值进行引用

//左值
void Swap(int& left, int& right)
{
 int temp = left;
 left = right;
 right = temp;
}
int main()
{
 int a = 10;
 int b = 20;
 Swap(a, b);
}

//右值
int Add(int a, int b)
{
 return a + b;
}
int main()
{
 const int&& ra = 10;
 
 // 引用函数返回值,返回值是一个临时变量,为右值
 int&& rRet = Add(10, 20);
 return 0;
}

为了与c++98中的引用进行区分,c++11将该种方式称之为右值引用

8.2.左值与右值

可以放在=左边的,或者能够取地址的称之为左值

只能放在=右边的,或者不能取地址的称之为右值

但这个说法不准确

int g_a = 10;
// 函数的返回值结果为引用
int& GetG_A()
{
 return g_a;
}
int main()
{
 int a = 10;
 int b = 20;
 
 // a和b都是左值,b既可以在=的左侧,也可在右侧,
 // 说明:左值既可放在=的左侧,也可放在=的右侧
 a = b;
 b = a;
 const int c = 30;
 // 编译失败,c为const常量,只读不允许被修改
 //c = a;
 // 因为可以对c取地址,因此c严格来说不算是左值
 cout << &c << endl;
 
 // 编译失败:因为b+1的结果是一个临时变量,没有具体名称,也不能取地址,因此为右值
 //b + 1 = 20;
 
 GetG_A() = 100;
return 0;
}

因此关于左值与右值的区分不是很好区分,一般认为:

1. 普通类型的变量,因为有名字,可以取地址,都认为是左值。

2. const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。

3. 右值通常是常量(注意不是const等修饰的,一般是1,2这种数字)、表达式、函数返回值等临时对象。

4. 左值通常是变量(注意const等修饰的是左值)左值一般都可以修改,取地址等操作。

int& a=10;//这样不行的,10是常量,是右值,不能左值引用

8.3 纯右值和将亡值

右值分为:纯右值和将亡值

纯右值:(传统的右值)基本内置类型的常量或临时变量、函数返回值等(函数返回值的本质就是一个临时变量)

将亡值:自定义类型的临时对象(c++11主要是为了解决这个)(有身份有地址的对象即将要被销毁了)

8.4 引用与右值引用比较

int main()
{
 // 普通类型引用只能引用左值,不能引用右值
 int a = 10;
 int& ra1 = a; // ra为a的别名
 //int& ra2 = 10; // 编译失败,因为10是右值
 
 const int& ra3 = 10;
 const int& ra4 = a;
 return 0;
}

两个概念:一个是普通引用 int&   一个是const引用 const int&

普通引用只能引用左值,不能引用右值

const引用能够引用左值,也能够引用右值

int main()
{
 // 10纯右值,本来只是一个符号,没有具体的空间,
 // 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
 int&& r1 = 10;
 r1 = 100;
 int a = 10;
 int&& r2 = a; // 编译失败:右值引用不能引用左值
 return 0;
}

右值引用只能够引用右值,在引用右值的过程当中,给10这个变量开辟临时变量有实际空间,r1是这个临时变量的别名,它的生命周期会被延长至和右值引用r1相同(原本临时对象用完就销毁,绑定到右值引用后会和引用一起存活到作用域结束)。注意:这个临时变量是编译器 “隐式创建” 的匿名对象,你无法直接通过名字访问它,但右值引用r1是它的 “别名”—— 所以r1 = 100;实际是修改这个临时变量的值。

注意:有const右值引用,但意义不大,它的功能完全可以使用const左值引用代替

总结:左值和右值的概念,引用和右值引用的用法

问题:既然c++98中的const类型引用即可以引用左值,也可以引用右值,为什么c++11还要复杂的提出右值引用呢?

8.5 值的形式返回对象的缺陷

如果一个类当中涉及到资源管理(比如你new出来的堆空间),用户必须显示提供拷贝构造、赋值运算符重载已经析构函数,否则编译器将会自动生成一个默认的,此时就会出现浅拷贝问题

class String
{
public:
 String(char* str = "")
 {
 if (nullptr == str)
 str = "";
 _str = new char[strlen(str) + 1];
 strcpy(_str, str);
 }
 
 String(const String& s)
 : _str(new char[strlen(s._str) + 1])
 {
 strcpy(_str, s._str);
}
String& operator=(const String& s)
 {
 if (this != &s)
 {
 char* pTemp = new char[strlen(s._str) +1];
 strcpy(pTemp, s._str);
 delete[] _str;
 _str = pTemp;
 }
 return *this;
 }
 
 String operator+(const String& s)
 {
 char* pTemp = new char[strlen(_str) + strlen(s._str) + 1];
 strcpy(pTemp, _str);
 strcpy(pTemp + strlen(_str), s._str);
 String strRet(pTemp);
 return strRet;
 }
 
 ~String()
 { if (_str) delete[] _str;}
private:
 char* _str;
};
int main()
{
 String s1("hello");
 String s2("world");
 String s3(s1+s2);
 return 0;
}

上述代码完全可以运行,但是有个地方可以优化,这里的operator+

这里返回是以值的方式返回,比如会进行创建临时对象,临时对象的创建就需要调用拷贝构造函数,最后销毁strRet,临时对象赋值给s3后临时对象才销毁,strRet、临时对象、s3每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内存完全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大,那么就需要想出来一种策略去优化这种情况

8.6 移动语义

c++11提出了移动语义的概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题

在c++中如果需要实现移动语义,必须使用右值引用,上述String类增加移动构造

String(String&& s)
 : _str(s._str)
{ 
 s._str =nullptr;
}

 C++11 规定:函数返回的局部对象(值返回)会被隐式转换为将亡值—— 本质是告诉编译器 “这个对象马上要销毁了,可以安全地窃取它的资源”。

因为strRet对象的生命周期在创建好临时对象后就结束了,即将亡值,C++11认为其为右值,在用strRet构造临时对象时,就会采用移动构造,即将strRet中资源转移到临时对象中。而临时对象也是右值,因此在用临时对象构造s3时,也采用移动构造,将临时对象中资源转移到s3中,整个过程,只需要创建一块堆内存即可,既省了空间,又大大提高程序运行的效率。

注意:

1. 移动构造函数的参数千万不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效。

2. 在C++11中,仅当类中没有显式定义拷贝 / 析构时,。编译器会为类默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理时,用户必须显式定义自己的移动构造。

8.7 右值引用左值

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?
这是不对的,因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过 move 函数将左值转化为右 C++11 中, std::move() 函数 位于<utility>头文件中,该函数名字具有迷惑性,它 并不搬移任何东西, 本质是 “类型转换工具”—— 它能把左值(或右值)强制转换为「将亡值(右值引用类型)」 ,然后实现移动语义
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v1 = {1,2,3,4}; // v1 是左值

    // 1. 移动构造:用 std::move(v1) 将 v1 转为右值
    vector<int> v2 = std::move(v1); 
    // 此时 v1 被“掏空”(资源被窃取),但仍是合法的空对象

    // 2. 移动赋值:同样触发移动语义
    vector<int> v3;
    v3 = std::move(v2); // v2 被掏空,v3 获得资源

    return 0;
}
注意:move函数不移动数据,仅仅是改变值的标签,比如这里的v1类型还是vector<int>,只是有个标签(move函数给的标签)告诉编译器,告诉别的vector可以来v1窃取资源,并且仅仅是move这一瞬间是将亡值,后面还是左值
  • std::move(v1) 表达式:只是一个 “临时的将亡值”—— 这个表达式的结果是右值引用类型(T&&),但它是 “匿名的”,仅在使用它的那一刻(比如赋值、传参)被视为将亡值,用完后就失效了。

一般来说不这样使用move,一般都是为了解决刚刚的strRet这种返回值一直进行拷贝构造导致效率低下的场景

所以c++11中对于所有的容器基本都实现了两个版本

一个是左值引用,一个是右值引用(一般不使用const,因为右值一般需要改变,比如之前的你需要把s._str=nullptr)

右值引用的作用是针对一些右值,push_back会调用他们的移动构造去转移资源,所以可以提高效率

总结:右值引用做参数和做返回值,减少拷贝的本质是利用了移动构造和移动赋值,左值引用和右值引用本质的作用都是减少拷贝,右值引用的本质可以认为是弥补左值引用不足的地方

左值引用:

做参数:const T&x,这里即可以接受左值也可以接受右值,因为是引用所以是别名可以减少拷贝

做返回值:T&f(),但要注意这个返回值出作用域是否还存在

右值引用

做参数:T&&x,解决的是左值中不能使用移动构造,因为你push_back()内部在赋值的时候使用构造函数,如果是左值,就会调用拷贝构造,右值可以使用移动构造

做返回值:左值能够做返回值是因为你外部有变量,这个变量的周期不是在函数内部所以可以使用左值引用返回,但右值解决的是外部没有,比如T ret=f(),需要使用一个变量来接受返回值,此时可以减少拷贝(注意左值是因为你出来函数作用域这个变量的生命周期还在)

8.8 完美转发

完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用给的另一个函数

void Fun(int& x){cout<<"lvalue ref"<<endl;}
void Fun(int &&x){cout<<"rvalue ref"<<endl;}
void Fun(const int &x){cout << "const lvalue ref" << endl;}
void Fun(const int &&x){cout << "const rvalue ref" << endl;}
template<typename T>
void PerfectForward(T &&t){Fun(std::forward<T>(t));}
int main()
{
 PerfectForward(10); // rvalue ref
 
 int a;
 PerfectForward(a); // lvalue ref
 PerfectForward(std::move(a)); // rvalue ref
 const int b = 8;
PerfectForward(b); // const lvalue ref
 PerfectForward(std::move(b)); // const rvalue ref
 return 0;
}
int main()
{
 string s

注意,这里的参数是T&&,当类型是一个模板的时候,加上&&那就是万能引用,这样是可以接受左值也可以接受右值的,会根据传入的是左值还是右值来进行初始化

比如传10,10是右值,那就是int&&,所以T就会实例化成int,那最后是变为int&&

std::forward的作用是 “完美转发”—— 将万能引用接收到的实参,以其原始的左值 / 右值属性传递给后续函数(这里是Fun)。

因为右值引用会在第二次调用之后的参数传递过程中属性丢失,需要使用完美转发来保证其特性

注意:对于所有的引用函数,一旦传参,无论是左值还是右值,在函数内部都会变成左值,因为你传参过来,这个变量有了名字并且可寻址,所以函数内部会变成左值,如果不使用完美转发,就会一直是左值

总结:c++11中右值引用主要有以下作用

1.实现移动语义(移动构造与移动赋值)

2.给中间临时变量取别名

3.实现完美转发

8.9 vector新增的emplace_back究竟有何神奇之处

有人说emplace_back比push_back更高效,这种说法是完全不准确的,具体看应用场景

针对c++11,push_back实现了右值引用

针对场景一:已经构造好的对象,比如string s1,你传入push_back和emplace_back是一样的效率

针对场景二:直接传入参数,比如push_back("右值"),emplace_back("右值"),这个来说是emplace效率更高,因为你传参的时候需要调用一次string的构造函数构造出临时对象,然后再传给右值还需要调用移动构造,emplace_back不需要传参的时候进行构造函数构造出临时对象,就单纯的传参,然后再函数内部进行一次构造函数即可,全程没有临时对象,所以高效在于减少了临时对象

比如:

这里不看最后一次拷贝构造,最后一次是因为扩容,需要把原来的空间拷贝到新空间

这里明显多了一次移动构造

怎么实现的???

template <typename... Args>  // 可变参数模板:接收任意参数
void emplace_back(Args&&... args) {  // 万能引用:转发参数
    if (_size >= _capacity) reserve(...);  // 扩容检查

    // 关键:在 vector 的内存上,用传入的 args 直接构造 Person
    new (reinterpret_cast<void*>(&_data[_size])) Person(std::forward<Args>(args)...);

    ++_size;
}

可以看出,emplace_back使用了可变参数模板:接受任意参数,函数内部使用了完美转发直接调用构造函数构造person然后存在对应的内存上

emplace_back接受的是参数,也就是person构造函数的参数,也就是string 和int,不是person类型的对象,函数内部的new是定位new,直接通过完美转发和定位new调用person的构造函数,把args转发给Person的构造函数,在vector的内存地址(&_data[_size])上构造

emplace版本的特点就是有模板的可变参数的特点

9. lambda表达式

9.1 c++98中的一个例子

在c++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法

#include<algorithm>
#include <functional>
int main()
{
 int array[] = {4,1,8,5,3,7,0,9,2,6};
 
 // 默认按照小于比较,排出来结果是升序
 std::sort(array, array+sizeof(array)/sizeof(array[0]));
 
 // 如果需要降序,需要改变元素的比较规则
 std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
 return 0;
}

如果待排序元素为自定义类型,需要用户定义排序时的比较规则

对于一个类(商品类)-->要排序---->按什么排

如果你在类内重载operator > = <等,你这次按价格,下次按数量又要去改代码,所以你写了一个仿函数,针对不同的比较方式写不同的仿函数(但需要很多个,而且依赖命名,写函数也行)

struct Goods
{
 string _name;
 double _price;
};
struct Compare
{
 bool operator()(const Goods& gl, const Goods& gr)
 {
 return gl._price <= gr._price;
 }
};
int main()
{
 Goods gds[] = { { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, {"菠萝", 1.5} };
 sort(gds, gds+sizeof(gds) / sizeof(gds[0]), Compare());
 return 0;
}
随着 C++ 语法的发展, 人们开始觉得上面的写法太复杂了,每次为了实现一个 algorithm 算法, 都要重新去 写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了 极大的不便 。因此,在 C11 语法中出现了 Lambda 表达式。

9.2 lambda表达式

int main()
{
 Goods gds[] = { { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, {"菠萝", 1.5} };
 sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& l, const Goods& r)
 ->bool
 {
 return l._price < r._price;
 });
return 0;
}

上述代码就是使用c++11中的lambda表达式来解决的,可以看出lambda表达式实际是一个匿名函数

9.3 lambda表达式语法

ambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

1. lambda表达式各部分说明

[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,译器根据[]来判断接下来的代码是否为lambda函数捕捉列表能够捕捉上下文中的变量供lambda函数使用

(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起

省略

mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修

饰符时,参数列表不可省略(即使参数为空)

->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分

可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导

{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量

注意: lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空

因此C++11最简单的lambda函数为:[]{}; lambda函数不能做任何事情。

int main()
{
 // 最简单的lambda表达式, 该lambda表达式没有任何意义
 []{}; 
 
 // 省略参数列表和返回值类型,返回值类型由编译器推导为int
 int a = 3, b = 4;
 [=]{return a + 3; }; 
 
 // 省略了返回值类型,无返回值类型
 auto fun1 = [&](int c){b = a + c; }; 
 fun1(10)
 cout<<a<<" "<<b<<endl;
 
 // 各部分都很完善的lambda函数
 auto fun2 = [=, &b](int c)->int{return b += a+ c; }; 
 cout<<fun2(10)<<endl;
 
 // 复制捕捉x
 int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; }; 
 cout << add_x(10) << endl; 
 return 0;
}

通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。

2.捕获列表说明

捕捉列表描述了上下文中那些数据可以被 lambda 使用 ,以及 使用的方式传值还是传引用
[var] :表示值传递方式捕捉变量 var
[=] :表示值传递方式捕获所有父作用域中的变量 ( 包括 this)
[&var] :表示引用传递捕捉变量 var
[&] :表示引用传递捕捉所有父作用域中的变量 ( 包括 this)
[this] :表示值传递方式捕捉当前的 this 指针
注意:
a. 父作用域指包含 lambda 函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割
比如: [=, &a, &b] :以引用传递的方式捕捉变量 a b ,值传递方式捕捉其他所有变量
[& a, this] :值传递方式捕捉变量a this ,引用方式捕捉其他变量 c. 捕捉列表不允许变量重复传递,否则就会导致编 译错误 。 比如: [=, a] = 已经以值传递方式捕捉了所有变量,捕捉 a 重复
d. 在块作用域以外的 lambda 函数捕捉列表必须为空
e. 在块作用域中的 lambda 函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
f. lambda 表达式之间不能相互赋值 ,即使看起来类型相同
void (*PF)();
int main()
{
 auto f1 = []{cout << "hello world" << endl; };
 auto f2 = []{cout << "hello world" << endl; };
 // 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了
 //f1 = f2; // 编译失败--->提示找不到operator=()
 // 允许使用一个lambda表达式拷贝构造一个新的副本
 auto f3(f2);
 f3();
 // 可以将lambda表达式赋值给相同类型的函数指针
PF=f2;
PF();
return 0;
}

9.4 函数对象与lambda表达式

函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了 operator() 运算符的类对象。
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.49;
 Rate r1(rate);
 r1(10000, 2);
 // lamber
 auto r2 = [=](double money, int year)->double{return money*rate*year; };
 r2(10000,2);
return 0;
}

从上述使用方法看,函数对象与lambda表达式完全一样

函数对象将rate作为其成员变量,在定义对象时给出初始值即可

lambda表达式通过捕获列表可以直接将该对象捕获到

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值