• 算法工程师应该熟知的C++高级用法


    1. lambda表达式(C11)

    1.1 lambda表达式的组成

    [=]/*1*/ ()/*2*/ mutable/*3*/ throw()/*4*/ -> int/*5*/ {}/*6*/
    
    • 1
    • capture子句
    • 参数列表(optional)
    • 可变规范(optional)
    • 异常定义(optional)
    • 返回类型(optional)
    • 函数体

    1.2 即看即用

    语法:

    [capture](parameters)->return-type {body}
    
    • 1

    []叫做捕获说明符

    parameters参数列表

    ->return-type表示返回类型,如果没有返回类型,则可以省略这部分。

    我们可以这样输出"hello,world"

    auto func = [] () { cout << "hello,world"; };  
    func(); // now call the function
    
    • 1
    • 2

    变量捕获与lambda闭包实现

    string name;  
    cin >> name;  
    [&](){cout << name;}();
    
    • 1
    • 2
    • 3

    lambda函数能够捕获lambda函数外的具有自动存储时期的变量。函数体与这些变量的集合合起来叫闭包。

    [ ] 不截取任何变量
    [&} 截取外部作用域中所有变量,并作为引用在函数体中使用
    [=] 截取外部作用域中所有变量,并拷贝一份在函数体中使用

    [=, &foo] 截取外部作用域中所有变量,并拷贝一份在函数体中使用,但是对foo变量使用引用

    [bar] 截取bar变量并且拷贝一份在函数体重使用,同时不截取其他变量

    [x, &y] x按值传递,y按引用传递

    [this] 截取当前类中的this指针。如果已经使用了&或者=就默认添加此选项。

    1.3 基本概念和用法

    c++中,普通的函数是长这样子的:

    ret_value function_name(parameter) option { function_body; }
    
    • 1

    比如:

    int get_value(int a) const {return a++;}
    
    • 1

    lambda表达式定义了一个匿名函数,并且可以捕获所定义的匿名函数外的变量。它的语法形式是:

    [ capture ] ( parameter ) option -> return_type { body; };
    
    • 1

    其中:

    • capture 捕获列表
    • parameter 参数列表
    • option 函数选项
    • return_type 函数返回值类型
    • body 函数体

    比如:

    // defination
    auto lamb = [](int a) -> int { return a++; };
    
    // usage
    std::cout << lamb(1) << std::endl;  // output: 2
    
    • 1
    • 2
    • 3
    • 4
    • 5

    组成lambda的各部分并不是都必须存在的:

    1. 当编译器可以推导出返回值类型的时候,可以省略返回值类型的部分
    auto lamb = [](int i){return i;};   // OK, return type is int
    auto lamb2 = [] () {return {1, 2};};    // Error
    
    • 1
    • 2
    1. lambda表达式没有参数时,参数列表可以省略
    auto lamb = []{return 1;};      // OK
    
    • 1

    所以一个最简单的lambda表达式可以是下面这样,这段代码是可以通过编译的:

    int main()
    {
      []{};     // lambda expression
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    捕获列表

    捕获列表是lambda表达式和普通函数区别最大的地方。[]内就是lambda表达式的捕获列表。一般来说,lambda表达式都是定义在其他函数内部,捕获列表的作用,就是使lambda表达式内部能够重用所有的外部变量。捕获列表可以有如下的形式:

    • [] 不捕获任何变量
    • [&]引用方式捕获外部作用域的所有变量
    • [=]赋值方式捕获外部作用域的所有变量
    • [=, &foo] 以赋值方式捕获外部作用域所有变量,以引用方式捕获foo变量
    • [bar] 以赋值方式捕获bar变量,不捕获其它变量
    • [this] 捕获当前类的this指针,让lambda表达式拥有和当前类成员同样的访问权限,可以修改类的成员变量,使用类的成员函数。如果已经使用了&或者=,就默认添加此选项。

    捕获列表示例:

    #include 
    
    class TLambda
    {
    public:
        TLambda(): i_(0) { }
        int i_;
    
        void func(int x, int y) {
            int a;
            int b;
    
            // 无法访问i_, 必须捕获this,正确写法见l4
            auto l1 = [] () {
                return i_;
            };
    
            // 以赋值方式捕获所有外部变量,这里捕获了this, x, y
            auto l2 = [=] () {
                return i_ + x + y;
            };
    
            // 以引用方式捕获所有外部变量
            auto l3 = [&] () {
                return i_ + x + y;
            };
    
            auto l4 = [this] () {
                return i_;
            };
    
            // 以为没有捕获,所以无法访问x, y, 正确写法是l6
            auto l5 = [this] () {
                return i_ + x + y;
            };
            
            auto l6 = [this, x, y] () {
                return i_ + x + y;
            };
    
            auto l7 = [this] () {
                return ++i_;
            };
    
            // 错误,没有捕获a变量
            auto l8 = [] () {
                return a;
            };
    
            // a以赋值方式捕获,b以引用方式捕
            auto l9 = [a, &b] () {
                return a + (++b);
            };
    
            // 捕获所有外部变量,变量b以引用方式捕获,其他变量以赋值方式捕获
            auto l10 = [=, &b] () {
                return a + (++b);
            }
    
        }
    };
    
    
    int main()
    {
        TLambda a;
        a.func(3, 4);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    引用和赋值,就相当于函数参数中的按值传递和引用传递。如果lambda捕获的变量在外部作用域改变了,以赋值方式捕获的变量则不会改变。按值捕获的变量在lambda表达式内部也不能被修改,如果要修改,需使用引用捕获,或者显示的指定lambda表达式为 mutable

    // [=]
    int func(int a);
    
    // [&]
    int func(int& a);
    
    // ------------------------------------------
    
    int a = 0;
    auto f = [=] {return a;};   // 按值捕获
    a += 1;                    // a被修改
    cout << f() << endl;        // output: 0
    
    // ------------------------------------------
    
    int a = 0;
    auto f = [] {return a++;};      // Error
    auto f = [] () mutable {return a++;}; // OK
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    1.4 lambda表达式的类型

    上面一直使用auto关键字来自动推导lambda表达式的类型,那作为强类型语言的c++,这个lambda表达式到底是什么类型呢?lambda的表达式类型在c++11中被称为『闭包类型(Closure Tyep)』,是一个特殊的、匿名的、非联合(union)、非聚合(aggregate)的类类型。可以认为它是一个带有operator()的类,即防函数(functor)。因此可以使用std::functionstd::bind来存储和操作lambda表达式。

    std::function<int (int)> f = [](int a) {return a;};
    std::function<int (int)> f = std::bind([](int a){return a;}, std::placeholders::_1);
    std::cout << f(22) << std::endl;
    
    • 1
    • 2
    • 3

    对于没有捕获任何变量的lambda,还可以转换成一个普通的函数指针。

    using func_t = int (*)(int); 
    func_t f = [](int a){return a;};
    std::cout << f(22) << std::endl;
    
    • 1
    • 2
    • 3

    2. if的高级写法

    if (a!=b,b!=c,a!=c)
    
    • 1

    C++的if语句使用逗号表达式,逗号表达式与加减乘除本质上是一样的, 它的求值是从左向右依次对表达式求值,
    整个表达式的结果取逗号表达式中最后一个表达的的结果, 如果非零, 就会使 if 成立!
    上面相当于:

    a!=b;
    b!=c;
    if (a!=c)
    
    • 1
    • 2
    • 3

    3. 左值与右值(C11)

    本质上说,左值是可以被寻址的值,即左值一定对应内存中的一块区域,而右值既可以是内存中的值,也可以是寄存器中的一个临时变量。一个表达式被用作左值时,使用的是它的地址,而被用作右值时,使用的是它的内容。

    在cpp11中,右值又可以进一步分为纯右值(pure rvalue)和将亡值(expiring value);其中纯右值指的是临时变量或者不和变量关联的字面量;其中临时变量包括非引用类型的函数返回值,比如 int f()的返回值,和表达式的结果,比如(a+b)结果就是一个临时变量;字面量比如10,“abc”这些。将亡值是cpp11新增的,适合右值引用相关的概念,包括返回右值引用T&&的函数的返回值,std::move的返回值

    如何准确的判断一个表达式是不是左值呢?

    • 如果这个表达式可以出现在等号的左边,一定是左值;
    • 如果可以对一个表达式使用&符号去地址,它一定是左值;
    • 如果它“有名字”,则一定是左值;

    形如 const T&的常量左值引用是个万金油的引用类型,其可以引用非常量左值,常量左值和右值。而形如T&的非常量左值引用只能接受非常量左值对其进行初始化。

    int &a = 2;       # 非常量左值引用绑定到右值,编译失败
    int b = 2;        # 非常量左值变量
    const int &c = b; # 常量左值引用绑定到非常量左值,编译通过
    const int d = 2;  # 常量左值
    const int &e = c; # 常量左值引用绑定到常量左值,编译通过
    const int &b =2;  # 常量左值引用绑定到右值,编程通过
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    右值引用通常不能绑定任何左值,如果要绑定左值,需要使用std::move()将左值转换为右值;

    int a;
    int&& r1 = a; //非法,a是左值;
    int&& r2 = std::move(a); //合法,通过std::move()将左值转换为右值。移动语义是C++11新增的重要功能,其重点是对右值的操作。右值可以看作程序运行中的临时结果,右值引用可以避免复制提高效率
    
    • 1
    • 2
    • 3

    下表列出了在C++11中各种引用类型可以引用的值的类型。值得注意的是,只要能够绑定右值的引用类型,都能够延长右值的生命期。
    在这里插入图片描述

    4. 重载匹配

    在这里插入图片描述

    5. costexpr

    5.1 costexpr变量

    一般来说,在日益复杂的系统中确定变量的初始值到底是不是常量表达式并不是一件容易的事情。为了解决这个问题C++11允许将变量声明为costexpr类型以便由编译器验证变量的值是否是一个常量表达式

    变量声明为constexpr类型,就意味着一方面变量本身是常量,也意味着它必须用常量表达式来初始化:

    constexpr int mf = 20;
    constexpr int limit = mf + 1;
    constexpr float y{108};
    
    constexpr int i; // Error! Not initialized
    int j = 0;
     constexpr int k = j + 1; //Error! j not a constant expression
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果初始值不是常量表达式,就会发生编译错误

    5.2 costexpr函数

    除了能用常量表达式初始化constexpr变量声明为,还可以使用consstexpr函数。它是指能用于常量表达式的函数,也就是它的计算结果可以在编译时确定。

    例如下面的计算阶乘的constexpr函数。

    constexpr long long factorial(int n){
    
       return n <= 1? 1 : (n * factorial(n - 1));
    
    }
    
    constexpr long long f18 = factorial(20);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可以用它来初始化constexp变量。所有计算都在编译时完成,比较有趣的是像溢出这样的错误也会在编译期检出

    定义的方法就是在返回值类型前加constexpr关键字。但是为了保证计算结果可以在编译期就确定,函数体中只能有一条语句,而且该语句必须是return语句,因此,下面:

    constexpr int data(){
        const i = 1;
        return i;
    }
    
    • 1
    • 2
    • 3
    • 4

    无法通过编译。不过一些不会产生实际代码的语句在常量表达式函数的使用下,可以通过编译,比如说static_assert,using、typedef之类的。因此,下面:

    constexpr int f(int x){
    	static_assert(0 == 0, "assert fail.");
    	return x;
    }
    
    • 1
    • 2
    • 3
    • 4

    可以通过编译。

    • 在使用前必须已经有定义。

    5.3 if constexpr

    我们知道,constexpr将表达式或函数编译为常量结果。一个很自然的想法是,如果我们把这一特性引入到条件判断中去,让代码在编译时就完成分支判断,岂不是能让程序效率更高。C++17支持if constexpr(表达式),允许在代码中声明常量表达式的判断条件,如下:

    #include 
    
    template<typename  T>
    auto print_type_info(const T & t){
        if constexpr (std::is_integral<T>::value){
            return  t + 1;
        }else{
            return t + 0.01;
        }
    }
    
    int main() {
        std::cout << print_type_info(5) << "\n";
        std::cout << print_type_info(5.5) << "\n";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    6. Cpp函数指针

    在Cpp中,函数名不可以作为形参或者返回参数,但是函数指针可以

    #include 
    #include
    
    using namespace std;
    
    void show(string s, int i) {
        while (i--) cout << s << endl;
    }
    
    // 返回函数指针
    // decltype返回的是函数类型,而函数只能返回函数指针
    decltype(show) *getShow() {
        return show;
    }
    
    auto getShow2() -> void (*)(string, int) {
        return show;
    }
    
    // F是函数名
    typedef decltype(show) F;
    // PF是函数指针
    typedef decltype(show) *PF;
    
    using FF = decltype(show);
    using PFF = decltype(show) *;
    
    using FFF = void(string, int);
    using PFFF = void (*)(string, int);
    
    void recvFunc(void(*f)(string, int), string s, int i) {
        f(s, i);
    }
    
    void recvFunc2(PFF f, string s, int i) {
        f(s, i);
    }
    
    void recvFunc3(F *f, string s, int i) {
        f(s, i);
    }
    
    
    int main() {
        void (*f1)(string, int);
        void (*f2)(string, int);
        // 对于函数指针,show 和 &show是等价的
        f1 = show;
        f2 = &show;
        f1("Hello", 1);
        f2("World !", 2);
    
    
        F *f3 = show;
        PF f4 = show;
        f3("Hello", 1);
        f4("World !", 2);
    
    
        FF *f5 = show;
        PFF f6 = show;
        f5("Hello", 1);
        f6("World !", 2);
    
        recvFunc(show, "Do!", 3);
        recvFunc2(show, "HA!", 3);
        recvFunc3(show, "LA!", 3);
    
        FFF *f7 = show;
        PFFF f8 = show;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    7. std::string_view(C17)

    std::string_view是C++ 17标准中新加入的类,正如其名,它提供一个字符串的视图,即可以通过这个类以各种方法“观测”字符串,但不允许修改字符串。由于它只读的特性,它并不真正持有这个字符串的拷贝,而是与相对应的字符串共享这一空间。即——构造时不发生字符串的复制。同时,你也可以自由的移动这个视图,移动视图并不会移动原定的字符串。

    const char *cstr_pointer = "pointer";
    char cstr_array[] = "array";
    std::string stdstr = "std::string";
     
    std::string_view
        sv1(cstr_pointer, 5),  // 使用C风格字符串-指针构造,并指定大小为5
        sv2(cstr_array),    // 使用C风格字符串-数组构造
        sv3("123456", 4),   // 使用字符串字面量构造,并指定大小为4
        sv4(stdstr),        // 使用std::string构造
        sv5("copy"sv);      // 使用拷贝构造函数构造(sv是std::string_view字面量的后缀)
     
    std::cout
        << sv1 << endl    // point
        << cstr_pointer << endl // pointer
        << sv2 << endl    // array
        << sv3 << endl    // 1234
        << sv4 << endl    // std::string
        << sv5 << endl;   // copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    7.1 span(C20)

    std::span是指向一组连续的对象的对象, 是一个视图view, 不是一个拥有者owner

    一组连续的对象可以是 C 数组, 带着大小的指针, std::array, 或者std::string

     #include 
     #include 
     #include 
     #include 
     #include void printSpan(std::span<int> container)
     {
         std::cout << std::format("container size: {} \n", container.size());
         for (auto ele : container)
         {
             std::cout << ele << " ";
         }
         std::cout << std::endl;
         std::cout << std::endl;
     }
     ​​
     int main()
     {
         std::vector v1{1, 2, 3, 4, 5, 8};
         std::vector v2{9, 2, 4, 2, 6, 78};
     ​
         std::span<int> dynamicSpan(v1);
         std::span<int, 6> staticSpan(v2);printSpan(dynamicSpan);
         printSpan(staticSpan);
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    8. stack、queue和priority_queue

    8.1 stack(栈)

    首先,你得写个头文件:

    #include 
    
    • 1

    那么如何定义一个栈呢?

    stack <类型> 变量名
    
    • 1

    接下来是一些关于栈的基本操作~

    stack <int> s;(以这个为例子)
    
    • 1

    1.把元素a加入入栈:s.push(a);
    2.删除栈顶的元素:s.pop();
    3.返回栈顶的元素:s.top();
    4.判断栈是否为空:s.empty();(为空返回TRUE)
    5.返回栈中元素个数:s.size();
    6.把一个栈清空:(很抱歉没有这个函数,你得写这些:)

    while (!s.empty())
    	s.pop();
    
    • 1
    • 2

    …详情请参照古月居

  • 相关阅读:
    计算机等级考试信息安全三级填空题-二
    数据结构实验四 线性表的基本操作及应用
    定期清理ES
    Python 获取线程返回值方法
    DMA+PWM驱动彩色RGB灯
    JMeter 二次开发之环境准备
    使用Socket和ServerSocket实现客户端和服务器端的数据传输
    汉诺塔与二进制、满二叉树的千丝万缕
    vscode使用delve调试golang程序
    dpi是什么?如何更改dpi?(仅个人笔记,可能有误)
  • 原文地址:https://blog.csdn.net/lovely_yoshino/article/details/127193408