C++11/14/17常用特性总结

1. C++11

1.1. nullptr常量

  • 说明:nullptr是nullptr_t类型的字面值,代表空指针,它可以被转换成任一其他的指针类型
  • 对比:
    1. 指针初始化为空指针:
        int* p1 = 0;        // C++98
        int* p2 = NULL;     // C++98
        int* p3 = nullptr;  // C++11
    
    1. 函数调用:
        void F(int);
        void F(void*);
    
        void Func() {
            F(0);           // 调用void F(int)
            F(NULL);        // 可能调用void F(int),取决于NULL的宏定义值
            F(nullptr);     // 调用void F(void*)
        }
    
  • 建议:优先选用nullptr, 而非0或NULL

1.2. constexpr关键字

  • 说明:
    1. 算术类型,引用,指针类型属于字面值类型
    2. constexpr用于声明变量时表示该变量是一个编译期常量,变量必须是字面值类型
    3. constexpr用于声明函数时表示该函数可以在编译期得出运算结果
    4. C++11中constexpr函数必须遵守以下约定,在C++14中有所放宽:
      1. 函数的返回类型和形参类型都必须是字面值类型
      2. 函数体只能是一条return语句,其他语句不能在运行时执行任何操作,例如空语句,类型别名声明语句等
    5. 调用constexpr函数时,如果传入的参数都是编译期常量,则函数结果将在编译期计算出来;如果传入参数有一个到多个的值编译期未知,则运作方式与普通函数无异
  • 对比:
    1. constexpr与const比较:constexpr对象一定是const对象,反之不成立
        int Add1(int a, int b);             // 普通函数
        constexpr int Add2(int a, int b);   // constexpr函数
    
        void Func() {
            const int c1 = 10;          // 编译期常量
            const int c2 = c1 + 1;      // 编译期常量
            const int c3 = Add1(1, 2);  // 运行时常量
            int x1 = 10;                // 非常量
    
            constexpr int ce1 = 10;             // 编译期常量
            constexpr int ce2 = ce1 + 1;        // 编译期常量
            // constexpr int ce3 = Add1(1, 2);  // 编译出错,Add1无法在编译期计算出结果
            constexpr int ce4 = Add2(1, ce1);   // 正确, 1和ce1都是编译期常量,Add2将在编译期计算出结果
            // constexpr int ce5 = Add2(1, x1); // 错误, x1不是编译期常量,Add2运作方式与Add1相同
    
            // 定义数组,数组维度必须是编译期常量
            int arr1[10];       // 正确
            int arr2[c1];       // 正确
            // int arr3[c3];    // 错误
            // int arr4[x1];    // 错误
            int arr5[ce1];      // 正确
        }
    
  • 建议:
    1. 如果认为变量是一个编译期常量,就为变量加上constexpr声明
    2. 只要有可能,就为函数加上constexpr声明

1.3. using类型别名声明

  • 说明:为某种类型定义另外一个名字, 格式: using alias = type;
  • 对比:
    1. using与typedef比较:
        // 为char*定义别名
        typedef char* cstring;
        using cstring = char*;
        // 定义函数指针类型别名
        typedef void(*pFunc)(int, int);     // pFunc是函数指针类型
        using pFunc = void(*)(int, int);    // pFunc是函数指针类型
        using Func = void(int, int);        // Func是函数类型, 不是指针类型,需要注意
    
  • 建议:
    1. 优先使用using进行别名声明,using声明更加直观易懂
    2. 不要使用#define定义类型别名,宏仅仅是字符串,没有类型信息

1.4. auto关键字

  • 说明:
    1. 使用auto定义变量时,编译器根据初始化表达式自动推导变量类型,因此变量必须初始化
    2. 使用auto可以定义持有lambda表达式的变量
  • 对比:
    1. 变量定义
        void Func() {
            // 初始化
            int x1;                 // 未初始化,存在风险
            // auto x2;             // 编译错误
            auto x3 = 4;            // x3是int类型
            auto x4 = x3;           // x4是int类型,拷贝x3的值
            auto& x5 = x3;          // x5是int&类型,指向x3
            const auto& x5 = x3;    // x6是const int&类型,指向x3
    
            // 避免类型转换
            std::vector<int> vec = {1, 2, 3, 4};
            int count1 = vec.size();    // 发生隐式类型转换, 编译告警
            auto count2 = vec.size();   // 正确接收size()函数的返回值
    
            // 简化变量定义
            std::map<int, std::string> int2StrMap;
            int2StrMap.insert(std::make_pair(1, "hello"));
            std::map<int, std::string>::iterator iter1 = int2StrMap.begin();    // C++98
            auto iter2 = int2StrMap.begin();                                    // C++11
    
            // 接收lambda表达式
            std::function<int(int)> func = [](int a) {
                return 5 + a;
            };
            auto lambda1 = [](int a) {
                return 5 + a;
            };
        }
    
  • 建议:
    1. 优先使用auto声明变量, 而非显式型别。可以简化定义,保证变量被初始化,消除型别不匹配问题
    2. 当使用大括号表达式初始化auto声明的变量时, 变量类型会被推导为std::initializer_list

1.5. 范围for语句

  • 说明:
    1. 范围for语句可以用来遍历容器的所有元素,格式为:
        for (declaration : expression) {
            statement
        }
    
    1. 范围for语句用于包含能返回迭代器的begin()和end()成员的对象
  • 对比:
    1. 遍历容器:
        std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9};
        // C++98
        for (std::vector<int>::iterator iter1 = vec.begin(); iter1 != vec.end(); ++iter1) {
            std::cout << *iter;
        }
        for (int i = 0; i < vec.size(); ++i>) {
            std::cout << vec[i];
        }
        // C++11
        for (auto val : vec) {
            std::cout << val;
        }
    
  • 建议:
    1. 当需要遍历整个容器而不需要元素的索引时,使用范围for语句而不是迭代器
    2. 当在范围for语句中使用auto时,内置类型的元素直接使用auto,需要修改元素时使用auto&,不需要修改元素时使用const auto&
    3. 一定不要在范围for语句中增删容器元素

1.6. lambda表达式与std::bind

  • 说明:
    1. 一个lambda表达式表示一个可调用的代码块,可以理解为一个匿名函数,其格式为:
        // 捕获列表中为lambda所在函数中定义的局部变量,如果没有mutable声明,则不能修改这些捕获的变量
        // 形参列表,返回类型,函数体与其他普通函数一样
        [捕获列表](形参列表) mutable -> 返回类型 {
            函数体
        }
    
    1. lambda表达式的结构中可以忽略形参列表、返回类型和mutable声明,但必须包含捕获列表和函数体:
        void Func() {
            auto f = []{
                return 5;
            }
            // 调用lambda表达式
            std::cout << f() << std::endl;
        }
    
    1. 忽略返回类型时,如果函数体只是一个return语句,则返回类型根据表达式推导,否则返回void
    2. lambda表达式的形参列表中不能有默认参数
    3. lambda表达式的捕获列表只用于捕获函数中的局部非static变量,而局部static变量和函数之外声明的名字可以直接使用
    4. lambda捕获方式:
        void Func() {
            int x1 = 5;
            const int x2 = 10;
            // 显式值捕获
            auto lambda1 = [x1, x2](int a) {
                return x1 + x2 + a;
            };
            // 显式值捕获x1,显式引用捕获x2
            auto lambda2 = [x1, &x2](int a) {
                return x1 + x2 + a;
            };
            // 默认值捕获x1,x2
            auto lambda3 = [=](int a) {
                return x1 + x2 + a;
            };
            // 默认引用捕获x1,x2
            auto lambda4 = [&](int a) {
                return x1 + x2 + a;
            };
            // 默认值捕获x1,显式引用捕获x2
            auto lambda5 = [=, &x2](int a) {
                return x1 + x2 + a;
            };
            // 默认引用捕获x1,显式值捕获x2
            auto lambda6 = [&, x2](int a) {
                return x1 + x2 + a;
            };
        }
    
    1. lambda本质上是一个函数对象(重载了operator()的类的对象):
         int x = 10;
         auto lambda1 = [x](int a) {
             return x + a;
         };
         // 等价于
         class TempUniqueName {
         public:
             TempUniqueName(int x) : x_(x)
             {}
    
             int operator()(int a) {
                 return x_ + a;
             }
         private:
             int x_;
         };
         TempUniqueName lambda1(x);
    
    1. std::bind是std::bind1st和std::bind2nd的后继特性, 它接受一个可调用对象,返回一个新的可调用对象以新的形参列表, 其格式为:
        auto newCallable = std::bind(callable, argList);
    
  • 对比:
    1. lambda,函数指针,函数对象比较:
        bool IntCompare(int a, int b) {
            return a > b;
        }
    
        class IntComparator {
        public:
            bool operator()(int a,int b) {
                return a > b;
            }
        }
    
        void Func() {
            std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9};
            // 函数指针
            std::sort(vec.begin(), vec.end(), IntCompare);
            // 函数对象
            std::sort(vec.begin(), vec.end(), IntComparator());
            // lambda
            std::sort(vec.begin(), vec.end(), [](int a, int b){
                return a > b;
            });
        }
    
    1. lambda, std::bind比较:
        void F1(int a1, int a2, double b1, double b2);
        // 包装F1,使其只需传入a1和b1参数, a2和b2使用给定值
        void Func() {
            // std::bind
            auto newF1 = std::bind(F1, std::placeholders::_1, 100, std::placeholders::_2, 10.0);
            newF1(50, 20.0);    // 等价于F1(50, 100, 20.0, 10.0)
            // lambda
            auto lambdaF1 = [](int a1, double b1) {
                F1(a1, 100, b1, 10.0);
            }
            lambdaF1(50, 20.0);
        }
    
  • 建议:
    1. 不要使用lambda的默认捕获方式,容易导致空悬指针问题
    2. 优先选用lambda,而不是std::bind:lambda可读性更好,表达力更强,可能运行效率也更高

1.7. =default/=delete

  • 说明:
    1. 对类的特种成员函数(默认构造函数,拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符,析构函数),可以使用=default来显式的要求编译器生成合成的版本,这些合成的版本会逐成员调用其对应的特种函数, 例如合成的默认构造函数: 对于内置类型,执行默认初始化(随机值),对于类类型,调用其默认构造函数
    2. 使用=default声明的函数是隐式内联的,如果不希望是内联函数,则需要将=default声明放在类外
    3. 对于任何函数(不仅是类的特种成员函数,虽然通常用于这些函数上)可以使用=delete要求编译器不定义这个函数,=delete声明必须出现在函数第一次声明的时候
    4. 如果类的析构函数被声明=delete,那么编译器将不允许创建该类型的变量或临时对象,因为无法销毁该类的成员
  • 对比:
        // = default
        class DefaultMember {
        public:
            DefaultMember() = default;                      // 合成默认构造函数, 隐式内联
            DefaultMember(const DefaultMember&);            // 合成拷贝构造函数, 非内联
            DefaultMember& operator=(const DefaultMember&); // 合成拷贝赋值运算符,非内联
            ~DefaultMember() = default;                     // 合成析构函数, 隐式内联
        };
        DefaultMember::DefaultMember(const DefaultMember&) = default;
        DefaultMember& DefaultMember::operator=(const DefaultMember&) = default;
    
        // = delete
        // C++98
        class NonCopyable1 {
            // 声明为private成员函数,不定义这些函数,无法访问,因此阻止了拷贝
            NonCopyable1(const NonCopyable1&);
            NonCopyable1& operator=(const NonCopyable1&);
        public:
            NonCopyable1();
            ~NonCopyable1();
        };
        // C++11
        class NonCopyable2 {
        public:
            NonCopyable2();
            ~NonCopyable2();
            // 删除的拷贝构造和拷贝赋值运算符
            NonCopyable2(const NonCopyable2&) = delete;
            NonCopyable2& operator=(const NonCopyable2&) = delete;
        };
    
  • 建议:
    1. 对于类的特种成员函数,如果希望使用编译器合成的版本,则使用=default显式声明这些函数
    2. 对于希望阻止拷贝的类应该使用=delete声明拷贝构造函数和拷贝赋值运算符,而不是将其声明为private成员

1.8. 右值引用与移动语义

  • 说明:
    1. 左值和右值:
      1. 左值:变量(包括形参,右值引用),函数,返回左值引用的函数调用,内建赋值表达式,前置自增自减表达式,指针解引用表达式,内建下标表达式,字符串字面量,强转为左值引用类型的表达式
      2. 右值:字面量(字符串字面量除外),返回非引用的函数调用,后置自增自减表达式,内建的算术表达式、逻辑表达式、比较表达式,取地址表达式,强转为非引用类型的表达式,枚举项,lambda表达式
    2. 右值引用就是必须绑定到右值的引用,使用&&表示, 右值引用不能绑定到一个左值上:
        int i = 42;
        int& r = i;             // r是左值引用
        // int&& rr = i;        // 错误, 右值引用不能绑定到左值
        // int& r2 = i * 42;    // 错误, i * 42是一个右值
        const int& r3 = i * 42; // 可以将const左值引用绑定到右值
        int&& rr2 = i * 42;     // rr2是右值引用
        // int&& rr3 = rr2;     // 错误, rr2也是左值
    
    1. 可以使用标准库函数std::move将一个左值转换成右值引用,当使用std::move后这个左值只能被赋值或销毁,而不能直接使用它:
        // 将左值i转换到右值,此后i可以被赋值,也可以直接销毁,在赋予i新值之前不能使用i的值
        int&& rr4 = std::move(i);
    
    1. 移动构造函数/移动赋值运算符:类的这两个特种成员函数的形参是该类对象的右值引用,在函数体中要完成资源的移动,同时还要确保移后对象处于这样一个状态-销毁它是无害的,一旦移动完成,源对象必须不再指向被移动的资源。这两个函数只进行资源的移动,而不进行资源分配,不会抛出异常,因此需要加上noexcept标识符,否则移动操作不会触发,编译器会转而调用拷贝操作:
        class Moveable {
            static constexpr int ARRAY_SIZE = 10;
        public:
            Moveable(int length = ARRAY_SIZE) : data_(new int[length]), len_(length)
            {}
            ~Moveable() {
                Free();
            }
            // 拷贝控制
            Moveable(const Moveable& other) : data_(new int[other.len_]), len_(other.len_) {
                for (int i = 0; i < len_; ++i) {
                    data_[i] = other.data_[i];
                }
            }
            Moveable& operator=(const Moveable& other) {
                // copy and swap,能正确处理自赋值
                Moveable tmp(other);
                std::swap(data_, tmp.data_);
                std::swap(len_, tmp.len_);
                return *this;
            }
            // 移动控制
            Moveable(Moveable&& other) noexcept : data_(other.data_), len_(other.len_) {
                other.data_ = nullptr;
                other.len_ = 0;
            }
            Moveable& operator=(Moveable&& other) noexcept {
                // copy and swap,能正确处理自赋值
                Moveable tmp(std::move(other));
                std::swap(data_, tmp.data_);
                std::swap(len_, tmp.len_);
                return *this;
            }
            // 对于拷贝赋值运算符和移动赋值运算符,有一种简洁的实现方法
            // 形参为Moveable类型,对左值调用拷贝构造,对右值调用移动构造
            Moveable& operator=(Moveable other) {
                std::swap(data_, other.data_);
                std::swap(len_, other.len_);
                return *this;
            }
    
        private:
            void Free() {
                if (data_) {
                    delete[] data_;
                }
            }
    
        private:
            int* data_;
            int len_;
        };
    
    1. 拷贝左值,移动右值,如果没有移动控制函数,那么对右值也将进行拷贝操作
  • 对比:
  • 建议:
    1. 对于使用std::move处理过的左值不要再使用其值
    2. 移动操作不是提高性能的灵药,应该假定移动操作不存在,成本高,未使用

1.9. explicit/override/final/noexcept指示符

  • 说明:

    1. 当类的构造函数只接受一个参数时,实际上定义了一种由该参数转换为该类类型的隐式转换规则,使用explicit可以抑制这种转换的发生:
        class ImplicitConvertable {
        public:
            ImplicitConvertable(const std::string&);
        };
    
        class ExplicitConvertable {
        public:
            explicit ExplicitConvertable(const std::string&);
        };
    
        void F1(const ImplicitConvertable& obj);
        void F2(const ExplicitConvertable& obj);
    
        void Func() {
            std::string str = "hello";
            F1(str);            // 正确,从str隐式构造一个ImplicitConvertable对象传入F1
            // F1("hello")      // 错误,编译器只会自动进行一步类型转换,这里需要两步转换
            F1(ImplicitConvertable(str));   //正确
    
            // F2(str);         // 错误,explicit抑制了隐式转换
            F2(ExplicitConvertable(str));    // 正确,只能显式构造ExplicitConvertable对象
        }
    
    1. 在继承体系中,可以使用override来显式说明意在重写基类中的虚函数:
        class Base {
        public:
            virtual void F1();      // 虚函数第一次出现的地方添加virtual关键字
            virtual void F2(int);
        };
    
        class Derived : public Base {
        public:
            void F1() override;     // 正确,重写基类虚函数void F1();
            // void F2() override;     // 错误,基类没有void F2()的虚函数
            // void F3() override;     // 错误,基类没有void F3()的虚函数
            virtual void F4();      // 正确, 派生类定义自己的虚函数
    
        };
    
    1. 使用final来阻止类被继承:
        class NoDerived final {};   // NoDerived不能被继承
        
        // class D : public NoDerived {};   // 错误, 不能从NoDerived继承
    
    1. 使用noexcept来制定一个函数不会抛出异常,这种函数可以简化调用处的代码,编译器也可以对该函数进行特殊优化.如果函数违反了异常说明而抛出了异常,那么程序将直接调用std::terminate结束程序:
        void Func() noexcept;   // 承诺Func不抛出异常
    
  • 对比:

  • 建议:

    1. 为类的单参数构造函数加上explicit指示符,可以防止隐式转换发生
    2. 在派生类中为想要改写的基类虚函数加上override指示符
    3. 为不想被继承的类加上final指示符
    4. 只要函数不抛出异常,就为其加上noexcept知识符

1.10. string数值转换函数

  • 说明:
    C++11中引入了一组用于数值和字符串之间相互转换的函数
  • 对比:
    1. 与C标准库字符串转换函数比较:
        // C库
        int atoi (const char * str);            // str转换到int
        long int atol ( const char * str );     // str转换到long
        double atof (const char* str);          // str转换到double
    
        // C++
        std::string to_string (val);        // val转换到字符串, val可以是任何算数类型
        int stoi (const string&  str, size_t* idx = 0, int base = 10);  // str 转换到int
        long stol (const string&  str, size_t* idx = 0, int base = 10); // str 转换到long
        float stof (const string&  str, size_t* idx = 0);               // str 转换到float
        double stod (const string&  str, size_t* idx = 0);              // str 转换到double
    
  • 建议: 使用C++风格的转换函数

1.11. std::array

  • 说明:
    1. std::array与数组一样拥有固定大小,并且大小需要在编译期给定
    2. std::array不会像数组一样作为参数传递时退化为指针
  • 对比:
        void Func {
            // 内置数组
            int arr1[10] = {0};
            for (auto val : arr1) {
                std::cout << val;
            }
            for (int i = 0; i < 10; ++i>) {
                std::cout << arr1[i];
            }
            // std::array
            std::array<int, 10> arr2;
            arr2.fill(0);
            std::cout << arr2.front();
            std::cout << arr2.back();
            for (auto val : arr2) {
                std::cout << val;
            }
            for (int i = 0; i < arr2.size(); ++i>) {
                std::cout << arr2.at(i);
            }
        }
    
  • 建议:
    1. 如果需要固定大小的数组,那么应该使用std::array而不是内置数组, 因为std::array类型安全,提供了丰富的接口,性能与内置数组相同

1.12. 无序关联容器

  • 说明:
    1. C++11定义了4个无序关联容器:unordered_map, unordered_set, unordered_multimap, unordered_multiset, 分别与有序关联容器:map, set, multimap, multiset对应
    2. 无序关联容器底层使用hash表实现, 因此不能假定容器中元素的排列顺序;有序关联容器底层使用红黑树实现,所有元素按照键值大小排列,遵循严格弱序准则
    3. 对于自定义类型,如果需要放入无序关联容器内时,需要给定该类型的hash计算方式和判等准则;需要放入有序关联容器时,只需要给定符合严格弱序的比较准则
    4. 无序关联容器的插入,查找,删除操作时间复杂度为O(1);有序关联容器的插入,查找,删除操作时间复杂度为O(lg(n))
  • 对比:
        // 自定义类型
        class Data {
        public:
            int Val();
            const std::string& Str();
        private:
            int val_;
            std::string str_;
        };
        // 比较准则,用于有序关联容器
        struct DataCompare {
            bool operator()(const Data& l, const Data& r) const {
                return l.Val() < r.Val() ||((l.Val() == r.Val()) && l.Str() < r.Str());
            }
        };
        // hash函数,用于无序关联容器
        struct DataHash {
            size_t operator()(const Data& data) const {
                return std::hash<int>()(data.Val()) ^ std::hash<std::string>()(data.Str());
            }
        };
        // 判等准则,用于无序关联容器
        struct DataEqual {
            bool operator()(const Data& l, const Data& r) const {
                return (l.Val() == r.Val()) && (l.Str() == r.Str());
            }
        };
    
        void Func() {
            std::set<Data, DataCompare> orderedDataSet;
    
            std::unordered_set<Data, DataHash, DataEqual> unorderedDataSet;
        }
    
  • 建议:
    1. 如果元素的顺序不重要,优先使用无序关联容器

1.13. 智能指针

  • 说明:
    1. C++11提供了两种智能指针:std::shared_ptr和std::unique_ptr; 一种伴随类:std::weak_ptr, 只能配合std::shared_ptr使用
    2. std::shared_ptr提供了共享所有权语义,可以拷贝赋值多次,并且是线程安全的,当指向资源的最后一个shared_ptr销毁时,资源被释放
    3. std::unique_ptr提供了独占所有权语义,同一时刻,只能有一个unique_ptr对象指向资源,unique_ptr不可拷贝赋值,只能移动
    4. std::weak_ptr是一种弱引用, 指向shared_ptr管理的资源,但不拥有所有权,每次使用前需要检查资源是否有效
    5. std::shared_ptr的对象大小是固定的, std::unique_ptr的对象大小是不定的
  • 对比:
        // 自定义类型
        struct Data {
            Data() : val(0), str()
            {}
    
            Data(int v, const std::string& s) : val(v), str(s)
            {}
    
            int val;
            std::string str;
        }
    
        void Func() {
            // std::uniqu_ptr
            std::uniqu_ptr<Data> up1;       // 空指针
            std::uniqu_ptr<Data> up2(new Data());   // 持有一个指向默认构造的Data对象的指针
            std::uniqu_ptr<Data> up3 = std::make_unique<Data>();// C++14,持有一个指向默认构造的Data对象的指针
            auto up4 = std::make_unique<Data>(1, "hello"); // C++14, 持有一个指向Data(1,"hello")对象的指针
            // auto up5 = up2   // 错误, unique_ptr不可拷贝
            auto up6 = std::move(up2);  // 正确,unique_ptr可移动,资源所有权转移到up6, up2变成空指针
    
            // std::shared_ptr与std::weak_ptr
            std::shared_ptr<Data> sp1;       // 空指针
            std::shared_ptr<Data> sp2(new Data());   // 持有一个指向默认构造的Data对象的指针
            std::shared_ptr<Data> sp3 = std::make_shared<Data>();// 持有一个指向默认构造的Data对象的指针
            auto sp4 = std::make_shared<Data>(1, "hello"); // 持有一个指向Data(1,"hello")对象的指针
            auto sp5 = sp2;     // 正确, shared_ptr可拷贝
            // auto sp6 = up3;  // 错误, 不能从unique_ptr拷贝资源
            auto sp7 = std::move(up3);  // 正确, 从unique_ptr移动资源
            std::weak_ptr<Data> wp1;    // 空指针
            std::weak_ptr<Data> wp2(sp2);    // 持有指向sp3所持资源的弱引用
            if (!wp2.expired()) {       // 判断与wp2关联的share_ptr是否已失效
                auto sp8 = wp2.lock();//若expired()为true, lock()返回空的shared_ptr,否则返回有效的shared_ptr
            }
            std::shared_ptr<Data> sp9(wp2); // 从weak_ptr构造shared_ptr, 如果若expired()为true则抛出异常
        }
    
  • 建议:
    1. 不要使用std::auto_ptr
    2. 优先使用std::unique_ptr, 因为unique_ptr有与原始指针相同的性能
    3. 优先使用std::make_unique()(C++14)和std::make_shared(), 因为只进行一次内存分配,内存占用更小,性能更好
    4. 使用std::weak_ptr来替代可能空悬的std::shared_ptr
    5. 使用std::weak_ptr来解决std::shared_ptr可能导致的指针环路

1.14. std::function

  • 说明:
    1. 可调用对象的概念: 函数,函数指针,函数对象,lambda表达式统称为可调用对象
    2. std::function内部可以保存一个可调用对象的副本,std::function对象本身也是可调用对象,调用std::function对象相当于调用其内部持有的可调用对象
  • 对比:
        // C++98, 一般只能通过函数指针传递可调用对象
        typedef int(*pAddFunc)(int, int);
    
        int Add(int a, int b) {
            return a + b;
        }
    
        void Invoke1(pAddFunc fn) {
            int val = fn(2, 3);
            std::cout << val;
        }
    
        void Func1() {
            Invoke1(Add);
        }
    
        // C++11
        using AddFunc = std::function<int(int, int)>;
    
        struct Adder {
            int operator()(int a, int b) {
                return a + b;
            }
        };
    
        void Invoke2(AddFunc fn) {
            int val = fn(2, 3);
            std::cout << val;
        }
    
        void Func2() {
            Invoke2(Add);
            Invoke2(Adder());
            Invoke2([](int a, int b) {
                return a + b;
            });
        }
    
  • 建议: 需要回调函数时,使用std::function接收可调用对象,而不是函数指针,可以简化调用逻辑

2. C++14

2.1. std::make_unique()

  • 说明:
    1. std::make_unique()在C++14中加入标准库,原因是在C++11时标准委员会忘记了~~~
    2. 使用方法见1.13
  • 对比:
  • 建议: 优先使用std::make_unique(),而非显式分配资源

2.2. 泛型lambda/初始化捕获

  • 说明:
    1. C++14中允许在lambda的形参列表中使用auto来接受任意类型的参数:
        // 可以接受任意类型的参数,只要该类型支持+运算
        void Func() {
            auto lambda1 = [](const auto& a, const auto& b) {
                return a + b;
            };
        }
    
    1. C++14中允许在lambda的捕获列表中使用初始化捕获的方式来捕获局部变量:
        void Func() {
            int num = 10;
            // 以初始化捕获的方式捕获局部变量num到val中
            // 需要注意: = 两侧是两个作用域, 左侧为lambda的作用域,右侧为函数的作用域
            auto lambda2 = [val = num](int a) {
                return val + a;
            };
        }
    
  • 对比:
    1. 泛型lambda:
        void Func() {
            // C++11, 有多少种类型,就需要定义多少种lambda
            std::vector<int> intVec = {1, 2, 3, 4, 5};
            auto lambda1 = [](int a) {
                std::cout << a;
            };
            std::for_each(intVec.begin(), intVec.end(), lambda1);
    
            std::vector<double> doubleVec = {1.0, 2.0, 3.0, 4.0, 5.0};
            auto lambda2 = [](double a) {
                std::cout << a;
            };
            std::for_each(doubleVec.begin(), doubleVec.end(), lambda2);
    
            std::vector<std::string> strVec = {"hello, ", "zhong ", "dian ", "xing ", "fa"};
            auto lambda3 = [](std::string a) {
                std::cout << a;
            };
            std::for_each(strVec.begin(), strVec.end(), lambda3);
    
            // C++14, 只需要定义一个泛型lambda
            auto lambda4 = [](auto a) {
                std::cout << a;
            }
            std::for_each(intVec.begin(), intVec.end(), lambda4);
            std::for_each(doubleVec.begin(), doubleVec.end(), lambda4);
            std::for_each(strVec.begin(), strVec.end(), lambda4);
        }
    
    1. 初始化捕获:
        void Func() {
            // 在lambda中捕获一个只可移对象
            std::unique_ptr<int> up1 = std::make_unique<int>(5);
    
            // C++11, 十分麻烦
    
            // C++14, 使用初始化捕获将up1移入lambda中
            auto lambda1 = [up2 = std::move(up1)]() {
                std::cout << *up2;
            };
        }
    
  • 建议:
    1. 使用泛型lambda表达式可以简化代码,提高代码复用能力
    2. 捕获列表中使用初始化捕获方式将只可移对象移入lambda中

2.3. 更加宽松的constexpr函数

  • 说明:
    1. C++14中放宽了constexpr函数的函数体不得包含多余一条可执行语句的规定,现在可以有任意多条可执行语句, 可以使用除了goto和try…catch以外的控制语句(if, while, for…)
  • 对比:
        // C++11
        constexpr int pow(int base, int exp) noexcept {
            return (exp == 0 ? 1 : base * pow(base, exp - 1));
        }
        // C++14
        constexpr int pow(int base, int exp) noexcept {
            auto result = 1;
            for (int i = 0; i < exp; ++i) {
                result *= base;
            }
            return result;
        }
    
  • 建议:

2.4. 二进制字面值和数字分隔符

  • 说明:
    1. 使用前缀0b后接01数字串,即可创建一个二进制数字
    2. 可以在数字中使用’单引号作为分隔符,提升数字的可读性
  • 对比:
        // 二进制数字
        // C++98及C++11:无
        //C++14
        int val = 0b1101000;    // val的值为104
    
        // 数字分隔符
        int num1 = 1000000000;      // C++98及C++11
        int num2 = 1'000'000'000;   // C++14
        int num3 = 1'00000'00'00;   // C++14,分隔符可以任意间隔
    
  • 建议:
    1. 对较大的数字字面值,使用数字分隔符间隔三位隔开,以提升可读性

3. C++17

3.1. 结构化绑定

  • 说明:
    1.结构化绑定 绑定指定名称到初始化器的子对象或元素,其格式为:
        const/static(可选) auto(必选) &/&&(可选) [逗号分隔的标识符列表] = 表达式;
        const/static(可选) auto(必选) &/&&(可选) [逗号分隔的标识符列表]{表达式};
        const/static(可选) auto(必选) &/&&(可选) [逗号分隔的标识符列表](表达式);
    
    1. 表达式可以是数组或非union类的任意类型
    2. 表达式是数组类型时:标识符列表中的每个标识符均成为指代数组的对应元素的左值。标识符的数量必须等于数组的元素数量:
        int a[2] = {1,2};
        auto [x,y] = a; // 创建 e[2],复制 a 到 e,然后 x 指代 e[0],y 指代 e[1]
        auto& [xr, yr] = a; // xr 指代 a[0],yr 指代 a[1]
    
    1. 表达式是类类型时:标识符列表中的每个标识符绑定到类的各个可访问非静态数据成员:
        struct S {
            int x1;
            double y1;
        };
        S f();
    
        auto [x, y] = f();  // x是x1的拷贝, y是y1的拷贝
    
  • 对比:
    1. std::map的循环遍历
        void Func() {
            std::map<int, std::string> int2StrMap;
            ...
            // C++98
            std::map<int, std::string>::iterator iter = int2StrMap.begin();
            for (; iter != int2StrMap.end(); ++iter) {
                std::cout << iter->first << ", " << iter-second;
            }
            // C++11
            for (const auto& pair : int2StrMap) {
                std::cout << pair.first << ", " << pair.second;
            }
            // C++17
            for (const auto& [val, str] : int2StrMap) {
                std::cout << val << ", " << str;
            }
        }
    
  • 建议:

3.1. 条件分支语句初始化

  • 说明:
    1. C++17中,在if/switch条件语句中可以增加初始化语句:
        if/switch (初始化语句; 条件)
        // 等价于
        初始化语句;
        if/switch (条件)
    
        void Func() {
            std::set<int> intSet;
            ...
            if (auto it = intSet.find(10); it != intSet.end()) {
                std::cout << *it;
            }
        }
    
  • 对比:
  • 建议:

3.1. std::filesystem

  • 说明:文件系统库
  • 对比:
  • 建议:

3.1. std::string_view

  • 说明:对字符序列或字符串切片的只读非占有引用
  • 对比:
  • 建议:

3.1. std::optional

  • 说明:用于表示可选对象
  • 对比:
  • 建议:

3.1. std::any

  • 说明:用于保存任何类型的单个值
  • 对比:
  • 建议:

3.1. std::variant

  • 说明:带有标记的联合容器
  • 对比:
  • 建议:

参考资料

  1. C++ Primer 5th : C++11大百科全书
  2. Effective Modern C++ : C++11/14的高效用法
  3. http://www.cplusplus.com/ : C++98/11参考手册
  4. https://zh.cppreference.com/w/cpp : C++最新标准参考手册
  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值