C++11新特性例举

C++11常用新特性例举

一、左值和右值

  1. 定义
    左值:表达式结束后仍旧存在的持久对象,有名称,可以取地址;
    右值:表达式结束后不再存在的临时对象,无名称,不可以取地址;例如,计算结果,常量等等;
    注:左值和右值都有常量和非常量之分;函数的返回值可以是常量右值也可以是非常量右值。

  2. 左值右值转换
    左值可以作为表达式的一部分,参与构成表达式(右值);
    右值一般不能够转换为左值,但是也可以通过表达式的方式,将表达式视作左值;例如*(p+1);

  3. 左值/右值引用
    左值引用:使用&来表示,相当于取别名;
    右值引用:使用&&来表示,即必须绑定到右值的一个引用,使用右值引用的代码可以自由地接管所引用的对象的资源,资源生命期跟随右值引用对象的生命期。另外,也可以视作临时对象本来没有名字,取第一个名字;
    注:右值引用对象是左值;另外,变量名实质上没有太大的意义,只是为了方便编程而引入的一个机制,编译器维持了一个符号表,在编译过程中,所有的变量名都将转换为对应的地址空间。(个人理解)

  4. 引用绑定规则

  • 非const左值引用只能绑定到非const左值;
  • const左值引用可绑定到const左值、非const左值、const右值、非const右值;
  • 非const右值引用只能绑定到非const右值;
  • const右值引用可绑定到const右值和非const右值;
    问题:非常量左值引用为什么不能够绑定到非常量右值?而常量左值引用可以绑定非常量右值?
  1. 引用折叠规则
  • 所有的右值引用叠加到右值引用上仍然还是一个右值引用;
  • 所有的其他引用类型的折叠情况都将变成左值引用;
  1. 其它
  • move函数: 能够显式地将一个左值转换为对应的右值引用类型,另外,调用了move函数就相当于承诺不再使用原来的左值变量;
  • forward函数: 对参数进行推导,当参数为左值/左值引用时,那么返回左值,参数为右值/右值引用时,返回右值;
    个人思考:右值引用相当于独占资源型的名称,左值引用相当于非独占资源型的名称。

二、移动语义

  1. 可拷贝和可移动
  • 可拷贝:面向对象程序设计中,有的对象是可以拷贝的,例如车、房等,可以调用拷贝构造函数;
  • 可移动:有些对象则是独一无二的,或者类的资源是独一无二的,比如IO,std::unique_ptr指向的资源对象等,他们不可以复制,但是可以把资源交出所有权给新的对象,称为可以移动的。
  1. 移动语义
  • 定义:在对象构造时直接获取已有的资源(如内存)而不是通过拷贝的方式申请新的内存,这样移动而非拷贝将会大幅度提升性能;—右值引用就是一种移动语义的体现,将一些临时对象通过右值引用的方式,转换为左值以保存;
  • 特性:浅层拷贝;将原先指向资源的引用/指针置为nullptr,防止浅拷贝的之后内存等资源被释放;移动构造函数需要先检查一下是否是自赋值,然后才能浅拷贝右值的成员,再delete右值原本的指针。
  • 示例:类A有一个无参构造函数,一个拷贝构造函数以及移动构造函数;现在定义一个新的变量A a(A());
    使用拷贝构造函数:需要进行两次内存分配,无参构造分配一次,拷贝构造分配一次;
    使用移动构造函数:只需要分配一次内存即可,仅无参构造分配一次;

三、完美转发

  1. 定义:函数/函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;同样如果相应实参是右值,它就应该被转发为右值,从而能够在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)的可能性。
  2. 完美转发举例:
    template<class A>
    void G(A &&a){
        F(forward<A>(a));
    }

对于上面模板函数:结合右值引用&&和forward实现完美转发;
对G传入左值,A被推导为T&,则a为左值引用;传入右值,a被推导为右值引用;
forward函数对参数进行推导,当参数为左值/左值引用时,那么返回左值,参数为右值/右值引用时,返回右值。

四、智能指针

  1. 定义:使用对象去管理一个资源指针,同时用一个计数器计算引用当前指针的对象个数,当管理指针的对象增加或减少时,计数器也相应加1或减1,当最后一个指针管理对象销毁时,此时计数器为1,此时在销毁指针管理对象的同时,也对指针管理对象所管理的指针进行delete操作。
  2. 常用的智能指针有:
  • std::unique_ptr: 资源独占型指针,最多有一个对象引用该指针;
  • std::shared_ptr: 共享型指针,指针引用对象数目不限,但每多一次引用则会使计数器加1;
  • std::weak_ptr:观察型指针/弱指针,引用与否不会引起计数器的变化,只能够对另外一个shared_ptr进行观察,另外,也没有重载*与->,没有资源的操作权限;
  • 注:weak_ptr一般用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放,使用weak_ptr进行代替即可;
    三种指针均可以自定义指针删除器,自定义函数或者lamda表达式;每一个指针对象在生命期结束时均自动析构;
    shared_ptr,weak_ptr,unique_ptr这些都是模板,并不是指针,只是引用了指针的对象;

五、lambda表达式

  1. Lambda表达式:可以用来创建匿名函数,并且,还可以捕获一定范围内的变量 (即直接在函数体中使用某变量,而不用在参数列表中声明)。
  2. 表达式格式:[capture list] (params list) mutable exception -> return type { function body}
  • capture list:捕获上下文外部变量列表, 指定this指针或者捕获变量的传递方式,值传递/引用传递;
    示例:[var]值传递捕获变量var; [=]值传递捕获lambda所有可见的局部变量/包括this;[&var], [&]引用捕获,类似;[this]捕获this指针;对外,对于不同的变量可以使用使用不同的传递方式;
  • params list:形参列表;
  • mutable指示符:用来说明是否可以修改捕获的变量,const/mutable;
  • exception:设定异常;
  • return type:返回类型,一般按照函数体内return语句设置,或者默认为空值;
  • function body:函数体;
  1. 用途:不需要提前设计好函数,在需要使用的时候再创建,例如,结合C++ STL使用,在排序时候定义比较函数。

六、可变参数模板

  1. 可变参数模板:C++11的可变参数模板,对参数进行了高度泛化,可以表示任意数目、任意类型的参数,其语法为:在class或typename后面带上省略号…”, 例如:template<class ... T> void func(T ... args) //T叫模板参数包,args叫函数参数包。
  2. 省略号’…'作用有两个:
  • 出现在参数左边:表示定义一个参数包,这个参数包中可以包含0到任意个模板参数;
  • 出现在函数参数包右边:表示将参数包展开成一个一个独立的参数,例如,args…表示参数展开。另外,sizeof…(args)可以用来计算参数个数;
  1. 可变参数函数模板:
        a. 递归方式展开:
        void print() {    // 递归结束函数
            cout << "empty" << endl;
        }
        template void print(T head, Args... args) {   // 递归函数
            cout << head << ","; print(args...);
        }
        b. 非递归方式展开:  
        template <class T>
        void print(T arg) { //打印函数
            cout << arg << endl;
        }
        template <class ... Args>
        void expand(Args ... args)  {   //展开函数
            (print(args), 0)... ;
        }
  1. 可变参数类模板:
        a. 递归方式class定义: 
        template<typename... A> class BMW{};  // 变长模板声明;
        template<typename Head, typename... Tail>  // 递归的偏特化定义
        class BMW<Head, Tail...> : public BMW<Tail...>
        {//当实例化对象时,则会引起基类的递归构造
        public:
            BMW() {
                printf("type: %s\n", typeid(Head).name());
            }
            Head head;
        };
        template<> class BMW<>{};  // 边界条件
        b. 递归方式struct定义:
        template <long... nums> struct Multiply;// 变长模板的声明
        template <long first, long... last>
        struct Multiply<first, last...> // 变长模板类
        {
            static const long val = first * Multiply<last...>::val;
        };
        template<>
        struct Multiply<> // 边界条件
        {
            static const long val = 1;
        };

七、C++11的四种类型转换

  1. static_cast:
    功能:完成编译器认可的隐式类型转换,type2 b = staic_cast<type2>(a);将a的类型转化为type2的类型;
    常见使用场景:
  • 基本数据类型之间的转换,如int->double;
  • 同一继承体系中父子类型的转换(上行转换相对来说比较安全,下行转换不安全);
  • 任意指针类型与空指针(void *)之间的转换(指针类型);
  1. dynamic_cast
    功能:执行派生类指针/引用与基类指针/引用之间的转换。
    常见使用场景:
  • 其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行运行时类型检查;转换成功的话返回指向类的指针/引用,否则会返回空;Dynamic_cast操作符则可以在运行期对可能产生问题的类型转换进行测试;
  • 基类中要有虚函数,因为运行时类型检查的类型信息在虚函数表中,有虚函数才会有虚函数表;
    个人理解:父子类中需要有虚函数的原因:类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义;–否则,使用基类指针/引用指向子类或者相反就没有意义了吧。
  • 可以实现向上转型和向下转型(会进行安全检查,相对于static_cast来说,更加安全),前提是必须使用public或protected继承;
  1. const_cast
    功能:对于同类型的指针/引用,在常量和非常量之间进行转换;
    注意,这里不是去掉变量的常量性质;而是去掉指向/引用常量对象的指针/引用对象的常量性质;让其也可以指向/引用正常变量;指针/引用变为非常量指针之后,并不意味着它们指向的值可以被改变(如果指向常变量的话);
    注意点:
  • 只能对指针或者引用去除或者添加const属性
  • 对于变量直接类型不能使用const_cast
  • 不能用于不同类型之间的转换,只能改变同种类型的const属性
  1. reinterpret_cast (谨慎使用)
    理论上来说,支持任意数据类型的转换,因为其仅仅是比特位的拷贝,因此在使用过程中需要特别谨慎!
    常见使用场景:
  • 指针/引用类型的相互转换;
  • 整数和指针/引用之间的相互转换;
  1. 四种类型转换常用场景总结:
    static_cast: 基本数据类型之间的转换(不包括指针/结构体/类等);不涉及虚函数的父子类型转换(下行转换是不安全的)
    const_cast: 指针/引用在const和volatile之间的转换,注意,是对指针/引用对象进行去/加常量性,而不涉及到其所指向的对象等等;
    dynamic_cast: 涉及虚函数的父子类型数据转换;运行时执行,相对比较安全;
    reinterpret_cast:指针/引用类型之间的转换;指针/引用和整数之间的相互转换;由于是位数据的直接拷贝,因此使用其要谨慎;
    –以上四种相互结合使用,基本能够满足我们在编程过程中遇到的所有需求;

问题:传统C的强制类型转换有什么缺点?(个人理解)

  • 转换过于粗鲁,允许任意类型之间的转换,对转换不进行区分;
  • C风格的类型转换在程序语句中难以识别。在语法上,类型转换由圆括号和标识符组成,而这些可以用在C++中的任何地方;而这种格式几乎可以应用在很多地方,例如,函数传参等等;

改进之后的优点(个人理解):

  • 对转换过程分型分类,更加规范;
  • 代码容易阅读,无论是编译器还是人工阅读;
  • 在一些地方进行类型检查,比如dynamic_cast, 无法转换的话会返回空值/异常,从而增强程序的鲁棒性

八、其它C++11新特性

  1. auto关键字:编译器可以根据初始化值自动推导出类型,但是不能用于函数传参以及数组类型的推导,经常和新式for循环以及lambda表达式一起使用;
  • a. auto定义变量时,必须对其进行初始化, 因为其仅仅是类型声明的“占位符”,编译器在编译期间会将auto替换为变量实际的类型。
  • b. auto 声明引用类型时需要加上&;
  • c. 使用auto定义多个变量时,变量的类型需要一致;
  • e. 不能够作为函数的形参;
  • f. 不能够用来声明数组;
  • g. 不能够用来定义类的非静态成员变量,因为auto类型的变量需要初始化;
  • h. 实例化模板时不能够使用auto作为参数,没有相关的推导信息;
  1. nullptr关键字:nullptr是一种特殊类型的字面值,表示指针空值常量,可以是任意类型的指针。

为什么要使用nullptr?
传统的NULL是一个宏,编译时会替换成0;有些时候会出现歧义,例如:F(int); F(int*);两个同名重载函数,那么现在调用F(NULL)时会调用前者,但本意应该是调用后者,因此有问题;所以,C++11中使用nullptr来进行替代;但同时保留了NULL以进行兼容;另外,NULL是宏,而nullptr是关键字;

  1. 新式for循环:
  • 使用方式:for(auto a : STL container){}
  • 使用条件:循环迭代的返回是确定的,容器的开始和结束;需要支持迭代器以及++和–;
  1. 初始化列表: 从花括号初始化器列表初始化对象,常见用于下列场景;
  • 普通数组,POD类型(使用memcpy复制的对象);
  • new操作符后面的数据/数组;
  • 使用到聚合类型:普通数组;或者类(无基类,虚函数,无自定义构造函数,无私有/保护的非静态数据成员,无{}/=直接初始化的非静态数据成员)
  • 非聚合类:自定义一个构造函数;调用构造函数进行初始化即可;
  • 容器,通过初始化列表进行任意长度的初始化;
  • 使用初始化列表,可以防止类型收窄(即类似于浮点数转为整形这种失真情况),进行报错;
  1. atomic原子操作
  • C++11,引入了原子操作的概念,并通过这个头文件#include<atomic>提供了多种原子操作数据类型,例如,atomic<bool>, atomic<int>等等,如果我们在多个线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的,也就是说,确保任意时刻只有一个线程对这个资源进行访问,编译器将保证,多个线程访问这个共享资源的正确性。从而避免了锁的使用,提高了效率。因此,利用atomic可以实现数据结构的无锁编程;
  1. array和tuple的引入
  • array-固定大小的数组;
  • tuple-可以看作pair的多元版本,pair仅仅支持两个相同/不同成员,而tuple支持任意数量的相同/不同类型成员;当需要将一些数据组合成单一对象,但又不想麻烦地定义一个新数据结构来表示这些数据时,std::tuple是非常有用的。我们可以将std::tuple看作一个”快速而随意”的数据结构。
  1. decltype(*), 用来获取括号中表达式类型,但并没有计算表达式的值;

九、其它c++知识点

  1. 虚函数表
    1. 虚函数表:每一个带有虚函数的类都有一个虚函数表,虚表是一个指针数组,每个元素对应一个虚函数的函数指针,指向其能访问到的最子代类的重载虚函数版本,虚表内的条目赋值在代码的编译阶段发生。
    2. 动态绑定:每一个类的对象最前方位置都有一个指针v_ptr指向自己所属类的虚函数表,其指向在类构造时就已经确定,因此,在运行时,不管是通过基类指针还是子类指针来调用对应的虚函数,都能够保证调用的虚函数是构造时类的虚表中对应条目指向的虚函数,从而保证函数的正确调用,实现动态多态特性;
    3. 虚函数和纯虚函数:
    • 虚函数:定义一个函数为虚函数,不代表函数为不被实现的函数,定义虚函数是为了允许用基类的指针来调用子类的这个函数,虚函数必须实现,否则会被报错,一般用于实现多态特性;
    • 纯虚函数:代表函数没有被实现,定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的子类必须实现这个函数;在基类中定义纯虚函数的方法是在函数原型后加“=0”,没有实现纯虚函数的类都不能够实例化,但可以声明指针/引用,用以指向具体子类的实例。
    • 注意点:
      只有成员函数才是虚函数,友元函数不能是虚函数,但可以通过调用虚函数的方式实现虚拟问题;
      析构函数应该是虚函数,调用的时候先调用子类的析构函数,再调用父类的析构函数;
      对于普通函数的话,尽管可以重载,但是无法使用父类指针去调用子类函数。
    1. 其它注意点
    • 普通函数调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针;
    • 虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可,同一个类的所有对象都使用同一个虚表。为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表;
    • 虚表中的指针会指向其继承的最近的一个类的虚函数,从本身向祖先回溯,直到找到最近的实现版本;
    • 虚表编译时就存在,创建对象时,对象隐藏的v_ptr指针就指向了该虚表,因此,不论怎么向上向下转型,v_ptr均属指向对象创建时指向的虚表;
    • 不管是使用对象,对象指针,对象引用,都只能够访问到对象的公有成员;
    • 父类指针没办法访问子类的公有成员,尽管指向了子类对象;
    • 子类指针/引用可以访问父类的公有成员;

注:以上为本人学习过程中所做笔记整理,如有错误,敬请指正。

参考文献

  1. C++11新增的四种强制类型转换
  2. 理解C和C++中的左值和右值
  3. C++ Lamda表达式
  4. c++11&14-智能指针专题
  5. C++ 11的移动语义
  6. 虚函数表详解
  7. C++11:可变参数的模板
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值