• 现代cpp教程笔记


    参考
    现代cpp教程
    https://changkun.de/modern-cpp/

    以下所有代码运行在win10+ VS2019+ cmake+ VS Code的环境下

    constexpr

    constexpr将表达式在编译器就计算好,从而运行时为一个常量。这就使得可以指定变量作为数组大小。

    用法1:指定数组大小

    #include 
    
    int main() {
        constexpr int len = 10;
        char arr[len];                      
        arr[1] ='c';
        std::cout << arr[1] <<std::endl;
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    去掉constexpr 关键字,在g++8.1上仍然通过,但是在msvc 2019上不通过。原因是g++将其自动优化了。

    用法2:分支预测

    可以用来预测参数的类型,在模板中有用。

    #include 
    
    template<typename T>
    bool isInt(T n){
        if constexpr(std::is_same<decltype(n), int>::value)
            return true;
        else
            return false;
    }
    
    int main() {
        std::cout << isInt(5) << std::endl;
        std::cout << isInt(3.14) << std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    其中if constexpre做了静态的分支判断。这是因为类型信息本来就是静态的(运行时常量)

    std::is_same::value
    用法是,先用decltype判断类型,然后用is_same判断类型是否相等。

    用法3:constexpr返回值的函数

    #include
    
    constexpr int fibonacci(const int n) {
        if(n == 1) return 1;
        if(n == 2) return 1;
        return fibonacci(n-1) + fibonacci(n-2);
    }
    
    int main(){
        std::cout<<fibonacci(10)<<std::endl;
        char a[fibonacci(10)];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    返回值不仅可以是constexpr,而且该函数还能递归。最后返还的值,可以作为数组大小。

    auto

    auto是个语法糖,用来自动推断类型。

    自动推导类型

    最简单的用法是这样:推导简单的类型

    int main(){
        auto i =5;
    }
    
    • 1
    • 2
    • 3

    或者这样:推导new返回的指针类型。

    int main(){
        auto p = new int(2);
    }
    
    • 1
    • 2
    • 3

    这就有点像是js中的let了。

    还可以这样推导自定义的类型

    #include 
    class A{
        int a,b;
    };
    int main(){
        auto pa = new A();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    简化迭代器、智能指针等冗长写法

    简化迭代器冗长写法

    #include 
    #include
    #include
    int main(){
        std::vector<int> vec{1,2,3,4};
        //for(std::vector::const_iterator it = vec.cbegin(); it != vec.cend(); ++it) //冗长写法
        for(auto it = vec.cbegin(); it != vec.cend(); ++it) //简化写法
            std::cout<<*it<<std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    std::vector::const_iterator被简化为了auto

    简化智能指针冗长写法

    #include
    #include
    int main(){
        //std::shared_ptr p = std::make_shared(5); //冗长写法
        auto p = std::make_shared<int>(5); //简化写法
        std::cout<<p<<std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    atuo推导函数返回值

    #include
    template<typename T, typename U>
    auto add(T x, U y){
        return x + y;
    }
    int main(){
        std::cout<<add(3,33)<<std::endl;//自动推导为整型
        std::cout<<add(3,3.3)<<std::endl;//自动推导为double型
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    非类型模板参数推导

    一般来说,在模板中给的typename xxx是个类型,比如int, double之类的。但其实我们还可以直接传递一个具体的参数,比如100。

    template <typename T, int BufSize> 
    //template  为更好的写法。
    class buffer_t {
    public:
        T& alloc();
        void free(T& item);
    private:
        T data[BufSize];
    };
    
    int main(){
    buffer_t<int, 100> buf; // 100 作为模板参数
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这里的具体参数100,叫做非类型模板参数。这样有点类似于常规的函数参数了。

    实际上,我们可以将其进一步泛化,把int BufSize改为auto BufSize
    这样,使用的时候仍然可以给100,而且我们在设计模板的时候无需关心类型。

    range for

    也是个语法糖,相当于不用定义迭代器了,直接在某个列表内循环(类似于python 的for i in List)

    #include 
    #include 
    #include 
    int main() {
        std::vector<int> vec = {1, 2, 3, 4};
        for (auto it = vec.begin(); it != vec.end(); ++it)//冗长写法
            std::cout << *it << std::endl; //冗长写法
    
        for (auto element : vec)//简化写法
            std::cout << element << std::endl; //简化写法
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    可见range for直接取值,而不是指针,所以不需要用*解引用。

    如果需要修改值的话,那就写为引用。

    #include 
    #include 
    int main() {
        std::vector<int> vec = {1, 2, 3, 4};
        for (auto &element : vec)
        {
            element+=1;
            std::cout << element << std::endl; 
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    tuple

    也是个语法糖。和python里面的元组类似。
    注:需要c++17

    函数返回元组以实现多个返回值

    #include 
    #include 
    
    auto f() {
        return std::make_tuple(1, 2.3, "456");
    }
    
    int main() {
        auto [x, y, z] = f();
        std::cout << x << ", " << y << ", " << z << std::endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    就是先把多个值打包成元组,然后接收返回值的时候再解包。

    越来越像python了…

    using

    using做别名替代typedef

    原本用typedef yyy xxx的地方都可以换成using xxx = yyy
    这样的好处是更加清晰。因为在typedef一个函数对象的时候,会造成心智负担。比如

    typedef int (*process)(void *);
    using NewProcess = int(*)(void *);
    
    • 1
    • 2

    把某个函数对象(其参数为一个void *,其返回值为一个int *)赋予别名process的时候,假如用typedef,就是上面那行。
    看起来很不直观。

    下面的新写法则简单直观得多。

    面向对象

    委托构造

    这是个语法糖。
    在写构造函数的时候,往往要重载多个版本。为了让功能一致的部分能够复用,可以采用委托构造。

    #include 
    class Base {
    public:
        Base() {
            std::cout << "in the first constructor" << std::endl;
        }
        Base(int value) : Base() { // 委托 Base() 构造函数
            std::cout << "in the second constructor" << std::endl;
        }
    };
    
    int main() {
        Base a;
        Base b(2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    输出

    in the first constructor
    in the first constructor
    in the second constructor
    
    • 1
    • 2
    • 3

    也就是调用第二个构造函数的时候,首先调用了第一个构造函数,然后调用第二个。相当于第二个重载的构造函数复用了第一个构造函数的部分,并且加上了自己的部分。这就实现了代码复用。

    假如将

     Base(int value) : Base() { // 委托 Base() 构造函数
    
    • 1

    改为

      Base(int value) { // 委托 Base() 构造函数
    
    • 1

    则输出

    in the first constructor
    in the second constructor
    
    • 1
    • 2

    也就是调用第二个构造函数的时候只调用第二个。

    继承构造

    这也是个语法糖。

    父类有多个版本的构造函数的时候,子类也必须一一对应着写多个版本,这很麻烦。
    使用using关键字,可以把父类的构造函数复用到子类中。

    原本冗长的写法是这样

    #include 
    class Base {
    public:
        int value1,value2;
        Base() {} //version1
        Base(int v1):value1(v1) {}//version2
        Base(int v1, int v2):value1(v1),value2(v2) {}//version3
    };
    
    class Subclass : public Base {
    public:
        Subclass():Base() {} //version1
        Subclass(int v1):Base(v1) {}//version2
        Subclass(int v1, int v2):Base(v1,v2) {}//version3
    };
    int main() {
        Subclass s(1,2);
    
        std::cout << s.value1 << std::endl;
        std::cout << s.value2 << std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    父类有几个构造函数,子类就要写几个。

    但是假如用using关键字,可以直接复用父类的构造函数

    #include 
    class Base {
    public:
        int value1,value2;
        Base() {} //version1
        Base(int v1):value1(v1) {}//version2
        Base(int v1, int v2):value1(v1),value2(v2) {}//version3
    };
    
    class Subclass : public Base {
    public:
        // Subclass():Base() {} //version1
        // Subclass(int v1):Base(v1) {}//version2
        // Subclass(int v1, int v2):Base(v1,v2) {}//version3
        using Base::Base;
    };
    int main() {
        Subclass s(1,2);
    
        std::cout << s.value1 << std::endl;
        std::cout << s.value2 << std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    假如Subclass新增了一个成员变量value3,那么只需要再重载一个版本的构造函数就好了。

    #include 
    class Base {
    public:
        int value1,value2;
        Base() {} //version1
        Base(int v1):value1(v1) {}//version2
        Base(int v1, int v2):value1(v1),value2(v2) {}//version3
    };
    
    class Subclass : public Base {
    public:
        int value3;
        // Subclass():Base() {} //version1
        // Subclass(int v1):Base(v1) {}//version2
        // Subclass(int v1, int v2):Base(v1,v2) {}//version3
        using Base::Base;
        Subclass(int v1, int v2, int v3):Base(v1,v2), value3(v3) {}
    };
    int main() {
        Subclass s(1,2,3);
    
        std::cout << s.value1 << std::endl;
        std::cout << s.value2 << std::endl;
        std::cout << s.value3 << std::endl;
    }
    
    • 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

    overide和final

    overide

    这也是语法糖。

    在以前,只需要在父类指定virtual就表示该函数要在子类中被重写。

    然而不需要在子类中的同名函数中给出任何标识(不需要virtual)。这就让虚函数看起来长得和普通函数完全一样。

    所以增加一个overide标识,表示该函数是被重写了的虚函数。

    这样做除了看上去好看之外,还有一个优点:就是防止不是虚函数的普通函数被重写了。一旦你试图这么做,编译器就会报错。(没错,virtual关键字和是否能被重写毫无关系,即使普通成员函数也能被重写,请看我之前的博客)。

    struct Base {
        virtual void foo(int);
    };
    struct SubClass: Base {
        virtual void foo(int) override; // 合法
        virtual void foo(float) override; // 非法, 父类没有此虚函数
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    报错

    使用“override”声明的成员函数不能重写基类成员C/C++(1455)
    
    • 1

    final

    也是语法糖
    它有两个用法

    保证类不再被继承

    struct Base {
        
    };
    struct SubClass final: Base {
    };
    struct SubSubClass: SubClass {
    };
    int main() {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    会报错

    error C3246: “SubSubClass”: 无法从“SubClass”继承,因为它已被声明为“final
    • 1

    保证虚函数不再被重写

    struct Base {
        virtual void func() final;
    };
    struct SubClass final: Base {
        void func();
    };
    
    int main() {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    会报错

    error C3248: “Base::func”: 声明为“final”的函数无法被“SubClass::func”重写 
    
    • 1

    =delete

    这是语法糖,用来
    禁用默认赋值构造函数和默认拷贝构造函数。

    以往的做法是把他们设定为private。

    很多时候我们都是不允许对象被拷贝或者赋值的,因为会造成内存泄漏(他们都是浅拷贝,一旦析构,指针所指的内容就丢失了。)

    class Magic {
        public:
        Magic() = default; // 显式声明使用编译器生成的构造
        Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成构造
        Magic(int magic_number);
    };
    int main() {
        Magic a;
        Magic b;
        b=a;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    会报错

    error C2280: “Magic &Magic::operator =(const Magic &): 尝试引用已删除的函数
    
    • 1

    Lambda表达式

    这个是重中之重。
    其实也可以看作一种语法糖,就是在原地定义了一个函数对象。

    用lambda替代函数

    最简单的一个例子

    #include 
    auto add = [](auto x, auto y) {//简化的写法
        return x+y;
    };
    
    template<typename T>//原本的写法
    auto old_add(T x, T y) {
        return x+y;
    }
    
    int main()
    {
        auto res = add(1, 2);
        auto res1 = old_add(1.1, 2.2);
        std::cout << res << std::endl;
        std::cout << res1 << std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    用lambda捕获外部变量

    如果只是单纯的把原本的函数换了个写法,这没什么的。lambda表达式的一个精髓在于它可以捕获外部变量。

    关键就在于[]。

    分两种捕获:值捕获和引用捕获

    值捕获

    #include 
    int main() {
        int value = 1;
        auto func = [value]() {
            return value;
        };
        auto v = func();
        std::cout<<v<<std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    打印结果
    1

    引用捕获

    #include 
    int main() {
        int value = 1;
        auto func = [&value]() {
            value = 2;
        };
        func();
        std::cout<<value<<std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    打印结果
    2

    自动捕获

    可以省略value,直接写成[=]或者是[&],前者是值捕获,后者是引用捕获。
    值捕获

    #include 
    int main() {
        int value = 1;
        auto func = [=]() {
            return value;
        };
        auto v = func();
        std::cout<<v<<std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    打印结果
    1

    #include 
    int main() {
        int value = 1;
        auto func = [&]() {
            value = 2;
        };
        func();
        std::cout<<value<<std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    打印结果
    2

    为捕获的参数赋予默认值

    #include 
    int main() {
        int v2 = 2;
        auto func = [v1=1, v2]() {
            return v1+v2;
        };
        auto res = func();
        std::cout<<res<<std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    输出3

    #include 
    int main() {
        int v2 = 2;
        auto func = [v1=1, &v2]() {
            v2 *= 10;
            return v1+v2;
        };
        auto res = func();
        std::cout<<res<<std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    输出21

  • 相关阅读:
    Java ClassLoader definePackage()方法具有什么功能呢?
    【Spring项目中的Service理解】
    Aiomysql 与 Sqlalchemy 的使用
    深度学习笔记Week4
    SpringBoot第49讲:SpringBoot定时任务 - 基础quartz实现方式
    计算机算法分析与设计(20)---回溯法(0-1背包问题)
    飞书开发学习笔记(三)-利用python开发调试云文档和电子表格
    Redis
    网络安全(黑客)——2024自学
    db-link 查询的语法约束
  • 原文地址:https://blog.csdn.net/weixin_43940314/article/details/126383585