C++11 新特性

C++11 新特性,auto/decltype自动类型推导、lambda表达式、移动语义、右值引用、智能指针、delete/default指示符等

自动类型推导

auto

auto 这个关键字 C++ 原先就有,用来指定存储器;但是这明显是多余的,没有必要;
因为很少有人去用这个东西,所以在 C++11 中就把原有的 auto 功能给废弃掉了,而变成了现在的类型推导关键字;

简单说一下 auto 的用法:

一般的,对于基本类型、结构体、类,并不推荐使用 auto 进行类型推导,因为这样的代码可读性不强;
但是对于诸如 函数指针、STL中某些复杂的类型、lambda表达式等,可以使用 auto,让代码更简洁;

从效率上来说,auto 不会对运行时效率产生影响,它是在编译的时候推导类型的;

auto和其他变量类型有明显的区别:

  1. auto 声明的变量必须要初始化,否则编译器不能判断变量的类型;
  2. auto 不能被声明为返回值,auto 不能作为形参,auto 不能被修饰为模板参数;

decltype

decltype 和 auto 是相互对应的,它们经常在一些场所配合使用;
decltype 可以在编译的时候判断出一个变量或者表达式的类型,例如:

auto 和 decltype 还有一种经典的使用场合,看下面例子:

函数模板 Add 用来计算两个变量之和,如果两个类型 T1 和 T2 不一样的话,我们就无法事先知道返回值的类型,这时候 auto 和 decltype 就派上出场了;

a + b的类型由编译器来决定,这样使用 decltype 就可以拿到返回的类型;
但是如果把decltype(a + b)放到函数命名的前面作为返回值的话,按照编译器的解析顺序,当解析到返回值decltype(a + b)的时候 a 和 b 还没有被定义,所以要重新声明 a 和 b 的类型,这样就会变的很复杂很难读懂;

于是 C++11 就改变了语法规则,把decltype(a + b)放到函数的后方,并用 auto 作为返回值来告诉编译器,真正的返回值在函数声明之后;简单的说 auto 可以作为返回值占位符来使返回值后置;

functional标准库

C++11 引入了函数对象标准库<functional>,里面包含各种内建的函数对象以及相关的操作函数,非常方便;

std::function
std::function类模板是一种通用的函数包装器,它可以容纳所有可以调用的对象(Callable),包括函数、函数指针、Lambda表达式、bind表达式、成员函数及成员变量或者其他函数对象;

std::bind
顾名思义,std::bind函数用来绑定函数的某些参数并生成一个新的function对象;
bind用于实现偏函数(Partial Function),相当于实现了函数式编程中的Currying(柯里化)

所谓偏函数,就是给一个原有的函数固定几个参数,形成一个新的函数,因此可以进行个性化定制;

看这个例子:

lambda表达式

Lambda 表达式来源于函数式编程,说白就了就是在使用的地方定义函数,有的语言叫闭包

lambda 表达式语法:[capture](parameters) mutable -> return-type { ... };

  • [capture]:捕捉列表;捕捉列表总是出现在Lambda函数的开始处;
    实际上,[]是Lambda引出符,编译器根据该引出符判断接下来的代码是否是Lambda函数;
    捕捉列表能够捕捉上下文中的变量以供Lambda函数使用;
  • (parameters):参数列表;与普通函数的参数列表一致;如果不需要参数传递,则可以连同括号“()”一起省略;
  • mutable:mutable修饰符;默认情况下,Lambda函数为一个const成员函数,mutable可以取消其常量性;
    在使用该修饰符时,参数列表不可省略(即使参数为空);
  • -> return-type:返回类型;用追踪返回类型形式声明函数的返回类型;
    如果没有返回值(例如 void),其返回类型可以完全省略;
    在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导;
  • { ... }:函数体;内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量;

与普通函数最大的区别是,除了可以使用参数以外,Lambda函数还可以通过捕获列表访问一些上下文中的数据;
捕捉列表描述了上下文中哪些数据可以被Lambda使用,以及使用方式(以值传递的方式或引用传递的方式);
语法上,在“[]”包括起来的是捕捉列表,捕捉列表由多个捕捉项组成,并以逗号分隔;捕捉列表有以下几种形式:

  • [=]:值传递方式捕捉所有父作用域的变量(包括 this);
  • [&]:引用传递方式捕捉所有父作用域的变量(包括 this);
  • [var]:值传递方式捕捉变量 var;
  • [&var]:引用传递方式捕捉变量 var;
  • [this]:值传递方式捕捉当前的 this 指针;
  • [&this]:引用传递方式捕捉当前的 this 指针;
  • 引用方式:因为 const 修饰对引用无效,所以通过引用捕获的变量可以在 lambda 表达式中进行读取、写入操作;
  • 值传递方式:因为默认添加了 const 进行修饰,所以只能读取捕获到的变量,无法进行写入操作;
    除非使用 mutable 修饰 lambda 表达式,这样就能够进行写入操作了,但是注意,因为是值传递方式,所以在 lambda 内部修改数据并不会影响外部变量的值;

上面提到了父作用域,也就是包含 Lambda 函数的语句块,说通俗点就是包含 Lambda 的“{}”代码块;

上面的捕捉列表还可以进行组合,例如:

  • [=, &a, &b]:以引用传递的方式捕捉变量 a 和 b,以值传递方式捕捉其它所有变量;
  • [&,a,this]:以值传递的方式捕捉变量 a 和 this,引用传递方式捕捉其它所有变量;

不过值得注意的是,捕捉列表不允许变量重复传递;下面一些例子就是典型的重复,会导致编译时期的错误;例如:

  • [=, a]:已经以值传递方式捕捉了所有变量,但是重复捕捉了 a,错误;
  • [&, &this]:已经以引用传递方式捕捉了所有变量,再捕捉 this 也是一种重复;

lambda 表达式的例子:

按值传递、引用传递的区别:

如果你搞不懂上面的代码为什么会产生这样的结果,那么请跟随我一起来探究一下 lambda 表达式的实现原理;

函数对象/仿函数
类的对象跟括号()结合,表现出函数一般的行为,这个对象可以称作是函数对象:

这个示例说明函数对象的本质是重载了函数调用运算符;
当一个类重载了函数调用运算符()后,它的对象就成了函数对象;这是理解 lambda 表达式内部实现的基础;

lambda表达式原理
原理:编译器会把一个lambda表达式生成一个匿名类的匿名对象,并在类中重载函数调用运算符;

1) 无捕获列表、无参数列表:[] () -> void { cout << "www.zfl9.com"; };
假设生成的匿名类的类名为TMP,编译器会将上面的 lambda 表达式改写为下面的形式:

2) 无捕获列表、有参数列表:[] (int a, int b) { return a + b; };

3) 值捕获:[name, age] () { cout << name << ", " << age << endl; };

4) 引用捕获:[&name, &age] () { age++; cout << name << ", " << age << endl; };

5) mutable修饰的值捕获:[name, age] () mutable { age++; cout << name << ", " << age << endl; };

泛型lambda

所谓泛型 lambda,就是在形参声明中使用 auto 类型指示说明符的 lambda;
比如:auto glambda = [] (auto a, auto b) { return a + b; };

根据 C++14 标准,这一 lambda 与以下代码作用相同:

C++14 的泛型 lambda 可以被看做 C++11 的(单态)lambda 升级版;
单态 lambda 相当于普通函数对象;而泛型 lambda 则相当于带模板参数的函数对象,或者说相当于带状态的函数模板;两者相比,可以推出以下结果:

  • 单态 lambda 在函数内使用,能够捕获外围变量形成闭包,作用相当于局部函数;泛型 lambda 强化了这一能力,其作用相当于局部函数模板;
  • 单态 lambda 能够服务于高阶函数(参数为函数的函数),作用相当于回调函数;泛型 lambda 强化了这一能力,使得泛型回调成为可能;
  • 单态 lambda 能够作为函数返回值,形成柯里化函数(闭包),用于 lambda 演算;泛型 lambda 强化了这一能力,使得泛型闭包成为可能;

可以说,泛型 lambda 大大加强了 C++ 中因单态 lambda 的引入而有所增强的 FP(函数型编程)能力;

相关的关键字

nullptr 空指针常量
nullptr是 C++11 标准用来表示空指针的常量值;
在 C 语言中,空指针的值表示为#define NULL ((void *)0)
在 C++ 中,由于对语法的类型检查更为严格,因而空指针的值就不能表示为(void *)0
所以至少自 C++98 开始#define NULL 0;但这会在函数重载时遇到新的困难,所以加入了 nullptr 来表示空指针;

override、final
在成员函数声明或定义中,override确保该函数为虚并重写来自基类的虚函数;若此非真则程序发生编译错误;
标记了final的虚函数不能被派生类重写,因此会将其从类的虚表中删除;而标记为final的类,编译器则根本不会生成虚表;并且不能被继承,为最终类;这样的代码显然更有效率;

mutable、const
mutable是可变的意思,mutable 用来修饰类的非静态非常量数据成员;
而被 mutable 修饰的数据成员,可以在 const 成员函数中修改;

在之前的 lambda 表达式原理解析中,使用了 mutable,不清楚的可以去参考一下;

delete、default
C++ 的编译器在你没有定义某些成员函数的时候会给你的类自动生成这些函数,比如:构造函数,拷贝构造,析构函数,赋值函数;
有些时候,我们不想要这些函数,比如,构造函数,因为我们想做实现单例模式;传统的做法是将其声明成 private 类型;

在 C++11 中引入了两个指示符,delete告诉编译器不自动产生这个函数,default告诉编译器产生一个默认的函数;

为什么我们需要default?我什么都不写不就是default吗?
不全然是,比如构造函数,因为只要你定义了一个构造函数,编译器就不会给你生成一个默认的了;所以,为了要让默认的和自定义的共存,才引入这个参数;

对于 delete 还有一个有用的地方,阻止函数的相关形参类型的调用:

noexcept
如果一个函数不能抛出异常,或者一个程序并没有接获某个函数所抛出的异常并进行处理,那么这个函数可以用新的noexcept关键字对其进行修饰,表示这个函数不会抛出异常或者抛出的异常不会被接获并处理;

例如:

如果一个经过 noexcept 修饰的函数抛出异常,程序会通过调用 terminate() 来结束执行;
通过 terminate() 的调用来结束程序的执行会带来很多问题,例如,无法保证对象的析构函数的正常调用,无法保证栈的自动释放,同时也无法在没有遇到任何问题的情况下重新启动程序;所以,它是不可靠的;

这和 C++11 之前的 throw() 异常规范一样的作用,但是比它却要高效得多;

同时,我们还可以让一个函数根据不同的条件实现 noexcept 修饰或者是无 noexcept 修饰;
声明的通常形式是noexcept(expression),并且单独的一个“noexcept”关键字实际上就是的一个noexcept(true)的简化;
expression是一个bool表达式,如果结果为 true,那么表示该函数不抛出异常,反之则可能抛出异常;

explicit
C++ 提供了关键字 explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生;声明为explicit的构造函数不能在隐式转换中使用:

没有使用 explicit 关键字的构造函数(一个参数):

使用 explicit 声明的构造函数:

委托构造

所谓委托构造就是:在一个构造函数中调用另外一个构造函数,这就是委托的意味;
不同的构造函数自己负责处理自己的不同情况,把最基本的构造工作委托给某个基础构造函数完成,实现分工协作;

比如:

初始化语法

C++ 之前的初始化语法很乱,有四种初始化方式,而且每种之前甚至不能相互转换;让人有种剪不断,理还乱的感觉;
1) 小括号初始化方法:int a = int(5);
2) 赋值初始化方法:int a = 3;
3) POD 聚合,也就是经常使用的大括号初始化方法:int arr[2] = {0, 1};
4) 构造函数初始化:

以上就是 C++03 之前的四种初始化方法,当然不是每种类型都有四种初始化方式;看到这里,估计你和我的感受差不多了,感觉这么多种初始化方式没必要呀,为毛不去统一一下,降低学习 C++ 的难度呢?

这不,C++ 的开发者们急我们之所急,在 N 年后的 C++11 中推出了统一初始化方法的新特性:统一使用花括号{}进行初始化

匿名命名空间

当定义一个命名空间时,可以忽略这个命名空间的名称:

编译器在内部会为这个命名空间生成一个唯一的名字,而且还会为这个匿名的命名空间生成一条 using 指令;所以上面的代码在效果上等同于:

在匿名命名空间中声明的名称也将被编译器转换,与编译器为这个匿名命名空间生成的唯一内部名称(即这里的__UNIQUE_NAME_)绑定在一起;

还有一点很重要,就是这些名称具有internal链接属性,这和声明为static的全局名称的链接属性是相同的;
名称的作用域被限制在当前文件中,无法通过在另外的文件中使用extern声明来进行链接;

注意:命名空间都是具有 external 链接属性的,只是匿名的命名空间产生的__UNIQUE_NAME_在别的文件中无法得到,这个唯一的名字是不可见的;

C++ 新的标准中提倡使用匿名命名空间,而不推荐使用 static,因为 static 用在不同的地方,含义不同容易造成混淆;

比如:带 static 的类成员为类共享,而变量前的 static 又表示内部链接、存储范围;
另外,static 不能修饰 class 定义,那样就可以将类定义放在匿名命名空间中达到同样的效果;

智能指针

什么是智能指针
智能指针是一个RAII(Resource Acquisition is initialization)类模型,用来动态的分配内存;它提供所有普通指针提供的接口,却很少发生异常;
在构造中,它分配内存,当离开作用域时,它会自动释放已分配的内存;这样的话,程序员就从手动管理动态内存的繁杂任务中解放出来了;

设计思想
将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写 delete 语句删除指针指向的内存空间;

unique_ptr

头文件:memory
unique_ptr 遵循着独占语义:在任何时间点,资源只能唯一地被一个 unique_ptr 占有;
当 unique_ptr 离开作用域时,所包含的资源被释放;如果资源被其它资源重写了,之前拥有的资源将被释放;所以它保证了他所关联的资源总是能被释放;

创建
new形式:unique_ptr<int> uptr(new int);
new[]形式:unique_ptr<int[]> uptr(new int[5]);

unique_ptr是具有以下特性的智能指针:

  • 通过指针保留了唯一的对象的所有权,并且 unique_ptr 离开作用域时,会析构指向的对象;
  • unique_ptr 不能复制或者复制赋值,两个 unique_ptr 实例不能管理同一个对象;
  • 一个非 const 的 unique_ptr 可以将所管理对象的所有权转移到另一个 unique_ptr;
  • 一个 const unique_ptr 不能转让,而是将所管理对象的生命周期限制在指针所创建的作用域之中;

成员函数
构造函数:构造新的 unique_ptr
析构函数:析构所管理的对象
operator=:为 unique_ptr 赋值
release:返回一个指向被管理对象的指针,并释放所有权
reset:替换所管理的对象
swap:交换所管理的对象
get:返回指向被管理对象的指针
get_deleter:返回删除器,用于被管理对象的析构
operator bool:检查是否有关联的被管理对象
operator*operator->:解引用操作
operator[]:提供对所管理数组的按索引访问

非成员函数
make_unique:创建管理对象的唯一指针(C++14)
swap(unique_ptr):特化 swap 算法
operator==operator!=operator<operator<=operator>operator>=:比较操作

例1:

例2:

shared_ptr

头文件:memory
shared_ptr是通过指针保持某个对象的共享拥有权的智能指针;
若干个 shared_ptr 对象可以拥有同一个对象;最后一个指向该对象的 shared_ptr 被销毁或重置时,该对象被销毁;
销毁该对象时使用的是 delete 表达式或者是在构造 shared_ptr 时传入的自定义删除器(deleter);

shared_ptr 也可以不拥有对象,称作空(empty);
shared_ptr 满足 CopyConstructible 和 CopyAssignable 的要求;

成员函数
构造函数:构造新的 shared_ptr
析构函数:如果没有更多 shared_ptr 指向持有的对象,则析构对象
operator=:为 shared_ptr 赋值
reset:替换所管理的对象
swap:交换所管理的对象
get:返回指向被管理对象的指针
operator*operator->:对所存储的指针进行解引用
use_count:返回 shared_ptr 所指对象的引用计数
unique:检查所管理对象是否仅由当前 shared_ptr 的实例管理
operator bool:检查是否有关联的管理对象
owner_before:提供基于拥有者的共享指针排序

非成员函数
make_shared:从参数创建并返回 shared_ptr,便于类型推断
get_deleter:返回指定类型的删除器,如果拥有的话
operator==operator!=operator<operator<=operator>operator>=:比较操作
operator<<:将所管理指针的值输出到输出流中
swap(shared_ptr):特化 swap 算法

实现说明
在典型的实现中,shared_ptr 只保存两个指针:

  • 指向被管理对象的指针
  • 指向控制块(control block)的指针

控制块是一个动态分配的对象,其中包含:

  • 指向被管理对象的指针或被管理对象本身
  • 删除器
  • 分配器(allocator)
  • 拥有被管理对象的 shared_ptr 的数量
  • 引用被管理对象的 weak_ptr 的数量

通过 make_shared 和 allocate_shared 创建 shared_ptr 时,控制块将被管理对象本身作为其数据成员;而通过构造函数创建 shared_ptr 时则保存指针;

shared_ptr 持有的指针是通过 get() 返回的;而控制块所持有的指针/对象则是最终引用计数归零时会被删除的那个;两者并不一定相等;

shared_ptr 的析构函数会将控制块中的 shared_ptr 计数器减一,如果减至零,控制块就会调用被管理对象的析构函数;但控制块本身直到 weak_ptr 计数器同样归零时才会释放;

例子:

weak_ptr

头文件:memory
weak_ptr是一种智能指针,它对被 shared_ptr 管理的对象存在非拥有性(“弱”)引用;在访问所引用的对象前必须先转换为 shared_ptr;

weak_ptr 用来表达临时所有权的概念:
当某个对象只有存在时才需要被访问,而且随时可能被他人删除时,可以使用 weak_ptr 来跟踪该对象;
需要获得临时所有权时,则将其转换为 shared_ptr,此时如果原来的 shared_ptr 被销毁,则该对象的生命期将被延长至这个临时的 shared_ptr 同样被销毁为止;

此外,weak_ptr 还可以用来避免 shared_ptr 的循环引用;

成员函数
构造函数:构造新的weak_ptr
析构函数:析构weak_ptr
operator=:为weak_ptr赋值
reset:释放被管理对象的所有权
swap:交换所管理的对象
use_count:返回shared_ptr所管理对象的引用计数
expired:检查被引用的对象是否已删除
lock:创建管理被引用的对象的shared_ptr
owner_before:提供基于拥有者的弱指针排序

非成员函数
swap(weak_ptr):特化 swap 算法

将一个 weak_ptr 赋给另一个 weak_ptr 会增加弱引用计数(weak reference count)

从 weak_ptr 调用 lock() 可以得到 shared_ptr;

当 shared_ptr 离开作用域时,其内的资源释放了,这时候指向该 shared_ptr 的 weak_ptr 将会过期(expired)

判断 weak_ptr 是否指向有效资源,有两种方法:

  • 调用use_count去获取引用计数,该方法只返回强引用计数,并不返回弱引用计数
  • 调用expired方法;比调用use_count方法速度更快;

例子:

右值引用

左值、右值
在 C++11 中所有的值必属于左值右值两者之一,右值又可以细分为纯右值将亡值
在 C++11 中可以取地址的、有名字的就是左值;反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)

右值、将亡值
在理解 C++11 的右值前,先看看 C++98 中右值的概念:
C++98 中右值是纯右值,纯右值指的是临时变量值不跟对象关联的字面量值;临时变量指的是非引用返回的函数返回值表达式等;

C++11 对 C++98 中的右值进行了扩充;在 C++11 中右值又分为纯右值(prvalue,Pure Rvalue)将亡值(xvalue,eXpiring Value)
其中纯右值的概念等同于我们在 C++98 标准中右值的概念,指的是临时变量不跟对象关联的字面量值
将亡值则是 C++11 新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用)

左值引用、右值引用
左值引用就是对一个左值进行引用的类型右值引用就是对一个右值进行引用的类型
事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在;

左值引用和右值引用都是属于引用类型;无论是声明一个左值引用还是右值引用,都必须立即进行初始化

左值引用通常也不能绑定到右值,但常量左值引用是个“万能”的引用类型;它可以接受非常量左值常量左值右值对其进行初始化;
不过常量左值所引用的右值在它的“余生”中只能是只读的;相对地,非常量左值只能接受非常量左值对其进行初始化

右值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值;

探究一下右值引用:
例一:

例二:

不管是左值引用还是右值引用,本质都是由指针实现的,非常量左值引用之所以不能绑定函数返回值、表达式结果、字面量等右值,是因为无法使用&取到它们的地址,因为它们要么存储在寄存器中(函数返回值、表达式结果),要么被硬编码到代码区(字面量);

而对于常量左值引用,编译器会创建一个临时变量,并将上述的值拷贝到该临时变量,因为不需要考虑引用与原数据之前的同步问题,所以创建一个临时变量反而增加了引用的灵活度和通用性;

并非所有的常量引用都会创建一个临时变量,编译器只会在必要的情况下创建临时变量,编译器有自己的判断能力;

而右值引用,本质上还是与左值引用一样,如果要绑定的数据不是临时数据(如例二中的move()语句),那么就等同于左值引用,并不会创建一个临时变量;如果要绑定的数据是临时数据,那么采取的机制和常量左值引用一样;常量右值引用也是一样的道理,只不过不能修改引用的值罢了(也仅仅是语义上的不能修改);

移动语义

C++11 新标准重新定义了 lvalue 和 rvalue ,并允许函数依照这两种不同的类型进行重载;
通过对于右值(rvalue)的重新定义,语言实现了移动语义(move semantic)完美转发(perfect forwarding)
通过这种方法,C++ 实现了在保留原有的语法并不改动已存在的代码的基础上提升代码性能的目的;

C++11 引入右值引用的概念,就是为了实现移动语义和完美转发;

对于类 Foo 来说:
CopyConstructible:拷贝构造函数,Foo(const Foo &foo);
CopyAssignable:拷贝赋值运算符,Foo & operator=(const Foo &foo);
MoveConstructible:移动构造函数,Foo(Foo &&foo);
MoveAssignable:移动赋值运算符,Foo & operator=(Foo &&foo);

如果一个类没有显示定义移动构造函数、移动赋值运算符,编译器并不会自动生成;而是使用拷贝构造函数、拷贝赋值运算符;

移动语义的使用场景:
我们先来看一下Copy语义的弊端:

先不管第一次编译出来的 a.out 的运行结果,我们看第二次的运行结果;
C/C++ 程序都是从 main 函数开始的,所以,当遇到语句Array arr = get_array(10000);时:
先执行赋值符号右边的语句,即调用函数 get_array,因为函数 get_array 直接 return 一个匿名的 Array 实例,为了防止离开函数 get_array 时自动调用 Array 的析构函数,编译器会创建一个临时变量,将 get_array 中的匿名对象拷贝过去(深拷贝);
而在函数 get_array 返回时,函数内的匿名对象将会被析构;然后再次将拷贝出来的临时变量赋值给 main 函数中的 arr,这又是一次深拷贝;拷贝结束后,这个临时变量的使命就完成了,于是调用析构函数,终结了自己的生命;最后 main 函数返回,arr 被析构;

注意到没有,使用Copy语义的情况下,函数 get_array 将会产生两次深拷贝的操作,这个开销是不容小觑的;

那为什么第一次编译的结果却只调用了构造函数和析构函数呢,并没有所谓的临时变量的产生过程?
那是因为现代编译器都采用了返回值优化技术,尽量避免了这种无意义的拷贝操作;

那既然有了返回值优化技术,为什么还需要Move语义?
因为仅仅依靠返回值优化技术不一定每次都能将该问题处理的很好,我们必须在语言层次上进行优化;

使用了移动语义的Array

因为函数 get_array 的返回值是一个右值,所以匹配到的构造函数就是Array::Array(Array &&arr),也就是所谓的移动构造函数
而对于 arr2 也是一样的道理,赋值操作的参数是一个右值(get_array 的返回值),匹配到的赋值函数就是Array & Array::operator=(Array &&arr),也就是所谓的移动赋值运算符;

如果你仔细观察这两个Move语义的成员函数,可以发现,这其实就是我们前面讲的”浅拷贝”,而Copy语义的成员函数就是”深拷贝”;

注意一个细节,arr.m_ptr 需要指向 nullptr,如果不这样做,可能导致拷贝出来的成员变量 m_ptr 变成悬置指针,因为被自动 free 掉了;

事实上左值和右值与类型是没有关系的,区别左值和右值的唯一方法就是其定义,即能否取到地址;也就是说,但凡有名字的“右值”,其实都是左值;使用std::move()可以将左值转换成右值;

完美转发

完美转发(perfect forwarding)问题是指函数模板在向其他函数传递参数时该如何保留该参数的左右值属性的问题;

也就是说函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;同样如果相应实参是右值,它就应该被转发为右值;
这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)的可能性;

如果将自身参数不分左右值一律转发为左值,其他函数就只能将转发而来的参数视为左值,从而失去针对该参数的左右值属性进行不同处理的可能性;

使用完美转发的典型场景:make_shared<T>(param...)make_unique<T>(param...),就必须使用完美转发;

例子:

static_assert断言

在 C++11 中,有 3 种错误处理机制:#errorassert()static_assert()

  • #error#error指令在预处理时有效,它将无条件地发出用户指定的消息并导致编译因错误而失败;该消息可包含由预处理器指令操作的文本,但不会计算任何生成的表达式;
    #error可看做预编译期断言,甚至都算不上断言,仅仅能在预编译时显示一个错误信息,它能做的不多,可以参与预编译的条件检查,由于它无法获得编译信息,当然就做不了更进一步分析了;
  • assertassert是运行期断言,它用来发现运行期间的错误,不能提前到编译期发现错误,也不具有强制性,也谈不上改善编译信息的可读性,既然是运行期检查,对性能当然是有影响的,所以经常在发行版本中,assert都会被关掉;
  • static_assert:进行编译时断言检查,语法格式:static_assert(bool_constexpr, message)bool_constexpr为常量表达式,message为字符串字面量;
    若 bool_constexpr 返回 true ,则此声明没有效果;否则发布一个编译时错误,而且若存在 message,则其文本被包含于诊断消息中;
    由于 static_assert 是编译期间断言,不生成目标代码,因此 static_assert 不会造成任何运行期性能损失;

强类型枚举

枚举类(“新的枚举”/“强类型的枚举”)主要用来解决传统的 C/C++ 枚举的三个问题:

  • 传统 C++ 枚举会被隐式转换为 int,这在那些不应被转换为 int 的情况下可能导致错误
  • 传统 C++ 枚举的每一枚举值在其作用域范围内都是可见的,容易导致名称冲突(同名冲突)
  • 不可以指定枚举的底层数据类型,这可能会导致代码不容易理解、兼容性问题以及不可以进行前向声明

枚举类(enum class)(“强类型枚举”)是强类型的,并且具有类域:

枚举类的底层数据类型必须是有符号无符号整型,默认情况下是int

同时,由于能够指定枚举值的底层数据类型,所以前向声明得以成为可能;
所谓前向声明就是在枚举类定义之前就使用这个枚举类的名字声明指针或引用变量;

比如上面例子中的 week 枚举类,可以这样做前向声明:enum class week : char;

for循环

以往,如果想要遍历一个数组,一般的做法是:

其实有些时候我们并不关心下标、迭代器位置或者元素个数,只是想依次输出元素的值而已,在 C++11 中可以这么写:

上面的代码中,因为 i 是按值传递的,所以在 for 循环体内部更改 i 的值并不会影响到外部的数组;
如果需要能够读写元素的值,那么可以将 i 改为按引用传递的方式,比如:

原始字符串

每次用 C 语言写正则模式的时候都非常蛋疼,各种转义,非常繁琐;
但是在 python 中写正则模式却是很爽的,因为可以定义一个不转义的原始字符串:r'raw_string'

那么在 C++ 中有没有类似的原始字符串的功能呢?
C++11 不愧称为 modern c++,当然会提供原始字符串了!

原始字符串字面量的定义为:R"xxx(raw_string)xxx"
其中,原始字符串必须用括号()括起来,括号的前后可以加其他字符串,所加的字符串会被忽略,并且加的字符串必须在括号两边同时出现;当然,最好两边什么也不加,看起来更清晰;

例子: