C++11新特性

本文主要总结深入理解c++11中的常用新特性

新标准的诞生

保证平稳性和兼容性

通用为本,专用为末

右值引用

在没有右值引用时,对带有指针成员变量的对象进行拷贝,会让指针的内存变得很难管理,甚至会产生错误。这是如果有移动语义,对象不执行拷贝,只执行移动就不会就这种错误。

在介绍移动语义之前,首先得明白左值,右值,和右值引用。

一般而言,可以取地址,有名字的就是左值。反之,不能取地址,没有名字的就是右值。我们称c++98中的引用为“左值引用”,而c ++11中的引用为右值引用。左值引用是一个具名对象的别名,而右值引用是一个不具名对象的别名。

右值引用延长了将亡对象的生命期。对应的,常量左值引用(const T &)也能延长一个将亡对象的生命期,但是付出的代价就是余生只能是只读的。

非常量左值引用只能绑定非常量左值

非常量右值引用只能绑定非常量右值

常量左值引用可以绑定任何值

常量右值引用没有用

右值引用一般的编写方式如下:


    class Moveable{
    public:
        Moveable(const Moveable& m);
        Moveable& operate=(const Moveable&);
        Moveable(Moveable&& m);
        Moveable& operate=(const Moveable&&);

std::move能够将一个对象的属性从左值变成右值,但是需要之一的是并不会改变对象的生命期。

在编写模板的时候,我们经常需要实现完美转发。例如:


    template <typename T>
    void IamForwording(T&& t)
    {
        IrunCodeActually(forward(t));
    }

使用forward可以实现完美转发,实际上,forward的功能和move一样,只不过在模板里面一般使用forward。它们都等于static_cast<T&&>,具体为什么可以去看“右值引用的模板推导”。

列表初始化

我觉得一般知道可以如下方式去初始化容器就够了。


    vector<int> a = {1, 2, 3, 4};
    map<int, float> b = {{1, 2.3f}, {2, 3.2f}};

POD类型

在c++11中,POD的概念被分为平凡和标准布局两个概念。平凡强调了类型没有复杂的构造函数,析构函数,多态等看起来不平凡的行为。而标准布局以为着可以通过memcpy进行拷贝。可以通过is_pod::value来判定一个类是否是POD。

下面是平凡的详细定义:

  • 拥有平凡的默认构造函数和析构函数。(就是不定义构造函数和析构函数)
  • 拥有平凡的拷贝构造函数和移动构造函数。(不定义)
  • 拥有平凡的拷贝复制运算符和移动赋值运算符。(不定义)
  • 不能包含虚函数以及虚基类。

标准布局的详细定义

  • 所有非静态成员都相同的访问权限。
  • 非静态成员不能同时出现在派生类和基类间。
  • 第一个非静态成员类型和基类不同。
  • 没有虚函数和虚基类。

新手易学,老手易用

  1. 常用auto和decltype。

auto不能推导的四种情况。


    void fun(auto x = 1){} //auto函数参数
    
    struct str{
        auto var = 10; //auto非静态成员变量
    }
    
    auto z[3] = x; //auto数组
    
    vector<auto> v = {1}; //auto模板参数
    
  1. 基于范围的for循环。
    vector<int> a = {1, 2, 3, 4};
    for(int i : a)
        cout << i << endl;
        

提高类型安全

强类型枚举

enum class Type { General, Light, Medium, Heaby };

它有如下优势:

  1. 强作用域,强类型枚举的名称不会被输出到其父作用域。
  2. 转换限制,值不可以和整形发生隐私转换。
  3. 可以指定底层类型,比如:
    
    enum class Type : char { General, Light, Medium, Heaby };

堆内存管理:智能指针与垃圾回收

智能指针

必用

垃圾回收

c++只实现了最简单的垃圾回收。了解即可。

垃圾回收一般可以分为两大类。

  1. 基于引用计数
    缺点是比较难处理“环形引用”。
  2. 基于跟踪
  • 标记-清除(标记:根据正在使用的对象查找引用的堆空间,并在堆空间上做标记。标记结束后,没有被标记的对象就是垃圾。清除:清除垃圾对象)
  • 标记-整理(整理:将活对象向左靠齐,还整理了碎片)
  • 标记-拷贝(拷贝:将堆分为FROM, TO,当某一块满了,将其中的活对象拷贝到另一块内存区域)

提高性能及操作硬件的能力

常量表达式

常量表达式的作用,就是在于有时候我们知道某个变量是编译器常量,但是因为不是const或者在函数内,所以编译器就是不给过。下面有个例子:


    int main()
    {
        int n = 3;
        int arr[n];
        return 0;
    }

在我们的教材中,要求定义arr必须使用编译器常量作为数组下标。所以,上面的写法应该是错的。但是!!!居然通过了编译,通过查阅资料,说这是c++的新特性。好吧,换个例子。


    int main()
    {
        int n = 3;
        enum { a = 3 }; //错误
        
        switch(cond){
            case getConst()://错误
                break;
        }
        
        return 0;
    }

看吧,上面报错了。

常量表达式函数

  • 函数体只有单一的return返回语句。
  • 函数必须有返回值(不能是void函数)。
  • 在使用前必须有定义。
  • return语句不能使用非常量表达式函数,全局数据,且必须是一个常量表达式。

常量表达式值

const int i = 1;
constexpr int j = 1;

它们几乎没有区别,区别我就不说了。

用常量表达式进行元编程

因为常量表达式是在编译器展开,所以递归调用就会产生模板元编程类似的效果。


    constexpr int Fibonacci( int n ){
        return (n == 1) ? 1 : ((n == 2) ? 1 : Fibonacci(n - 1) + Fibonacci(n -2));
    }

变长模板

  • 变长类模板常用方式


    //变长模板的声明
    template <typename... Elements> class tuple;
    
    //递归的偏特化定义
    template <typename Head, typename... Tail>
    class tuple<Head, Tail...> : private tuple<Tail...>{
        Head head;
    }
    
    //边界条件
    template<> class tuple<> {};
  • 变长函数模板常用方式

    #include <iostream>
    using namespace std;
    
    //结束条件
    void test()
    {
        return;
    }

    template<typename T, typename... Args>
    void test(T t, Args... args)
    {
        cout << t << endl;
        test(args...);
    }

    int main()
    {
        test(1,2,3,4,5,6,7,8);
    }

原子类型

  • 理解c++原子操作内存模型。

为了充分利用CPU的多级流水线模型,编译器处理器都可能将我们所写的代码顺序打乱。这可能对我们编写的进行原子操作的代码的正确性造成影响。因此,在使用原子类型的时候,必须对内存模型有所了解。

一般而言,常用的内存模型有以下几种:

  • memory_order_seq_cst : 全部按照顺序执行(原子操作默认的内存模型)。
  • memory_order_acquire : 所有后续读操作都在本条操作后面完成。
  • memory_order_release : 所有之前的写操作必须在本条之前完成。

通常而言,我们可以通过memory_order_acquire,memory_order_release写出无锁的高性能程序。

    atomic<int> a;
    atomic<int> b;
    int Thread1(int){
        int t = 1;
        a.store(t, memory_order_relaxed);
        b.store(2, memory_order_release); //本操作之前的所有写原子操作必须完成。
    }
    
    int Thread2(int){
        //本操作之后的读原子操作必须等该条语句执行。
        while(b.load(memory_order_acquire) != 2);
        cout << a.load(memory_order_relaxed) << endl;
    }
    

线程局部存储

    int thread_local errCode;

很简单,不解释。

quick_exit和at_quick_exit

因为exit()需要在函数结束的时候调用全局对象的析构函数,所以如果析构函数里面有加锁,或则delete之类的操作,在多线程的情况下可能对产生错误。

quick_exit与exit的区别就是不执行全局对象的析构函数。
at_quick_exit的作用是注册退出时执行的函数的。

为改变思考方式而改变

指针空值——nullptr

一般情况下,NULL是个宏定义

#define NULL 0

可能会产生二义性。

注意:nullptr不能像bool隐式转换。
if(nullptr)和if(nullpty == 0)之类的用法都是不允许的。

默认函数的控制

=default;使用默认函数版本。
=delete;删除编译器生成的默认函数。

lambda函数

一般格式如下:
auto fun = [=](int a)->int{return a};

  • [] : 里面是捕捉列表,能够捕捉父作用域中的变量。(使用值传递,需要注意捕捉列表里面的值不会随外界改变)
  • () : 参数,可以省略。
  • ->return-type :返回值,能够推导出的情况可以省略。
  • {} : 函数体。

lambda函数是仿函数的语法糖,所有lambda函数都能转换为一个仿函数。

融入实际应用

对齐支持

c++11可以获取和指定数据的对齐方式。

alignas(8) char c;//指定为按8为对齐
int n = alignof(double)//获取按多少位数据对齐

//可以将ptr指向的大小为space的内存的对齐方式进行调整,将ptr开始的size大小的数据调整为按alignment对齐。
align(std::size_t alignment, std::size_t size, void *&ptr, std::size_t& space);
原文地址:https://www.cnblogs.com/biterror/p/6909608.html