nullptr 出现的目的是为了替代 NULL。
在某种意义上来说,传统 C++ 会把 NULL、0 视为同一种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定义为 ((void*)0),有些则会直接将其定义为 0。
C++ 不允许直接将 void * 隐式转换到其他类型,但如果 NULL 被定义为 ((void*)0),那么当编译char *ch = NULL;时,NULL 只好被定义为 0。
而这依然会产生问题,将导致了 C++ 中重载特性会发生混乱,考虑:
void foo(char *);
void foo(int);
对于这两个函数来说,如果 NULL 又被定义为了 0 那么 foo(NULL); 这个语句将会去调用 foo(int),从而导致代码违反直观。
为了解决这个问题,C++11 引入了 nullptr 关键字,专门用来区分空指针、0。
nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。
当需要使用 NULL 时候,养成直接使用 nullptr的习惯。
2. 类型推导
C++11 引入了 auto 和 decltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。
auto
auto 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 register 并存。在传统 C++ 中,如果一个变量没有声明为 register 变量,将自动被视为一个 auto 变量。而随着 register 被弃用,对 auto 的语义变更也就非常自然了。
使用 auto 进行类型推导的一个最为常见而且显著的例子就是迭代器。在以前我们需要这样来书写一个迭代器:
for(vector<int>::const_iterator itr = vec.cbegin(); itr != vec.cend(); ++itr)
而有了 auto 之后可以:
for(auto itr = vec.cbegin(); itr != vec.cend(); ++itr);
注意:1.auto 不能用于函数传参
2. auto类型不能出现在顶级数组类型中(不能用于推导数组类型)eg.
int arr[10] = {0};
auto auto_arr = arr; //正确 auto_arr 为int* 类型
auto auto_arr2[10] = arr; //错误 auto类型不能出现在顶级数组类型中
decltype
decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。编译器分析表达式并得到它的类型,却不实际计算表达式的值。
eg.
TestClass* c3 = nullptr;
decltype(c3) c4; //c4为TestClass* 类型
3. 区间迭代基于范围的 for 循环
C++11 引入了基于范围的迭代写法,最常用的 std::vector 遍历将从原来的样子:
std::vector<int> arr(5, 100);
for(std::vector<int>::iterator i = arr.begin(); i != arr.end(); ++i) {
std::cout << *i << std::endl;
}
变得非常的简单:
// & 启用了引用
for(auto &i : arr) {
std::cout << i << std::endl;
}
4. 初始化列表
C++11 提供了统一的语法来初始化任意的对象, 还把初始化列表的概念绑定到了类型上,并将其称之为 std::initializer_list,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁.
eg.
struct A
{
int a;
float b;
};
A a {1, 1.1}; // 统一的初始化语法
5. 模板增强
外部模板
传统 C++ 中,模板只有在使用时才会被编译器实例化。只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化。这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知编译器不要触发模板实例化。
C++11 引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使得能够显式的告诉编译器何时进行模板的实例化:
eg.
template class std::vector<bool>; // 强行实例化
extern template class std::vector<double>; //不在该编译文件中实例化模板
尖括号 “>”
在传统 C++ 的编译器中,>>一律被当做右移运算符来进行处理。但实际上我们很容易就写出了嵌套模板的代码:
eg.
std::vector<std::vector<int>> wow;
这在传统C++编译器下是不能够被编译的,而 C++11 开始,连续的右尖括号将变得合法,并且能够顺利通过编译。
类型别名模板
在传统 C++中,typedef 可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称。因为,模板不是类型。例如:eg.
template< typename T, typename U, int value>
class SuckType {
public:
T a;
U b;
SuckType():a(value),b(value){}
};
template< typename U>
typedef SuckType<std::vector<int>, U, 1> NewType; // 不合法
C++11 使用 using 引入了下面这种形式的写法,并且同时支持对传统 typedef 相同的功效:eg.
template <typename T>
using NewType = SuckType<int, T, 1>; // 合法
默认模板参数
我们可能定义了一个加法函数:eg.
template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
return x+y
}
但在使用时发现,要使用 add,就必须每次都指定其模板参数的类型。
在 C++11 中提供了一种便利,可以指定模板的默认参数:eg.
template<typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
6. 构造函数
委托构造
C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的:eg.
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托 Base() 构造函数
value2 = 2;
}
};
继承构造
在继承体系中,如果派生类想要使用基类的构造函数,需要在构造函数中显式声明。
假若基类拥有为数众多的不同版本的构造函数,这样,在派生类中得写很多对应的“透传”构造函数。如下:
eg.
struct A
{
A(int i) {}
A(double d,int i){}
A(float f,int i,const char* c){}
//...等等系列的构造函数版本
};
struct B:A
{
B(int i):A(i){}
B(double d,int i):A(d,i){}
B(folat f,int i,const char* c):A(f,i,e){}
//......等等好多个和基类构造函数对应的构造函数
};
C++11的继承构造:
eg.
struct A
{
A(int i) {}
A(double d,int i){}
A(float f,int i,const char* c){}
//...等等系列的构造函数版本
};
struct B:A
{
using A::A;
//关于基类各构造函数的继承一句话搞定
//......
};
如果一个继承构造函数不被相关的代码使用,编译器不会为之产生真正的函数代码,这样比透传基类各种构造函数更加节省目标代码空间。
7. Lambda 表达式
Lambda 表达式,实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。
Lambda 表达式的基本语法如下:
[ caputrue ] ( params ) opt -> ret { body; };
1) capture是捕获列表;
2) params是参数表;(选填)
3) opt是函数选项;可以填mutable,exception,attribute(选填)
mutable说明lambda表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的对象的non-const方法。
exception说明lambda表达式是否抛出异常以及何种异常。
attribute用来声明属性。
4) ret是返回值类型(拖尾返回类型)。(选填)
5) body是函数体。
捕获列表:lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。
1) []不捕获任何变量。
2) [&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
3) [=]捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。注意值捕获的前提是变量可以拷贝,且被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝。如果希望lambda表达式在调用时能即时访问外部变量,我们应当使用引用方式捕获。
4) [=,&foo]按值捕获外部作用域中所有变量,并按引用捕获foo变量。
5) [bar]按值捕获bar变量,同时不捕获其他变量。
6) [this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量。
eg.
class A
{
public:
int i_ = 0;
void func(int x,int y){
auto x1 = [] { return i_; }; //error,没有捕获外部变量
auto x2 = [=] { return i_ + x + y; }; //OK
auto x3 = [&] { return i_ + x + y; }; //OK
auto x4 = [this] { return i_; }; //OK
auto x5 = [this] { return i_ + x + y; }; //error,没有捕获x,y
auto x6 = [this, x, y] { return i_ + x + y; }; //OK
auto x7 = [this] { return i_++; }; //OK
};
int a=0 , b=1;
auto f1 = [] { return a; }; //error,没有捕获外部变量
auto f2 = [&] { return a++ }; //OK
auto f3 = [=] { return a; }; //OK
auto f4 = [=] {return a++; }; //error,a是以复制方式捕获的,无法修改
auto f5 = [a] { return a+b; }; //error,没有捕获变量b
auto f6 = [a, &b] { return a + (b++); }; //OK
auto f7 = [=, &b] { return a + (b++); }; //OK
注意f4,虽然按值捕获的变量值均复制一份存储在lambda表达式变量中,修改他们也并不会真正影响到外部,但我们却仍然无法修改它们。如果希望去修改按值捕获的外部变量,需要显示指明lambda表达式为mutable。被mutable修饰的lambda表达式就算没有参数也要写明参数列表。
原因:lambda表达式可以说是就地定义仿函数闭包的“语法糖”。它的捕获列表捕获住的任何外部变量,最终会变为闭包类型的成员变量。按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量的值的。而mutable的作用,就在于取消operator()的const。
ambda表达式的大致原理:每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,是一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。对于复制传值捕捉方式,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。对于引用捕获方式,无论是否标记mutable,都可以在lambda表达式中修改捕获的值。至于闭包类中是否有对应成员,C++标准中给出的答案是:不清楚的,与具体实现有关。
8. 新增容器
std::array
std::array 保存在栈内存中,相比堆内存中的 std::vector,我们能够灵活的访问这里面的元素,从而获得更高的性能。
std::array 会在编译时创建一个固定大小的数组,std::array 不能够被隐式的转换成指针,使用 std::array只需指定其类型和大小即可:eg.
std::array<int, 4> arr= {1,2,3,4};
int len = 4;
std::array<int, len> arr = {1,2,3,4}; //非法数组大小参数必须是常量表达式
当我们开始用上了 std::array 时,难免会遇到要将其兼容 C 风格的接口,这里有三种做法:
eg.
void foo(int *p, int len) {
return;
}
std::array<int 4> arr = {1,2,3,4};
// C 风格接口传参
// foo(arr, arr.size()); // 非法, 无法隐式转换
foo(&arr[0], arr.size());
foo(arr.data(), arr.size());
// 使用 `std::sort`
std::sort(arr.begin(), arr.end());
std::forward_list
std::forward_list 是一个列表容器,使用方法和 std::list 基本类似。
和 std::list 的双向链表的实现不同,std::forward_list 使用单向链表进行实现,提供了 O(1) 复杂度的元素插入,不支持快速随机访问(这也是链表的特点),也是标准库容器中唯一一个不提供 size() 方法的容器。当不需要双向迭代时,具有比 std::list 更高的空间利用率。
无序容器
C++11 引入了两组无序容器:
std::unordered_map/std::unordered_multimap 和 std::unordered_set/std::unordered_multiset。
无序容器中的元素是不进行排序的,内部通过 Hash 表实现,插入和搜索元素的平均复杂度为 O(constant)。
元组 std::tuple
元组的使用有三个核心的函数:
std::make_tuple: 构造元组
std::get: 获得元组某个位置的值
std::tie: 元组拆包
9. 正则表达式
正则表达式描述了一种字符串匹配的模式。一般使用正则表达式主要是实现下面三个需求:
1) 检查一个串是否包含某种形式的子串;
2) 将匹配的子串替换;
3) 从某个串中取出符合条件的子串。
C++11 提供的正则表达式库操作 std::string 对象,对模式 std::regex (本质是 std::basic_regex)进行初始化,通过 std::regex_match 进行匹配,从而产生 std::smatch (本质是 std::match_results 对象)。
我们通过一个简单的例子来简单介绍这个库的使用。考虑下面的正则表达式:
[a-z]+.txt: 在这个正则表达式中, [a-z] 表示匹配一个小写字母, + 可以使前面的表达式匹配多次,因此 [a-z]+ 能够匹配一个及以上小写字母组成的字符串。在正则表达式中一个 . 表示匹配任意字符,而 . 转义后则表示匹配字符 . ,最后的 txt 表示严格匹配 txt 这三个字母。因此这个正则表达式的所要匹配的内容就是文件名为纯小写字母的文本文件。
std::regex_match 用于匹配字符串和正则表达式,有很多不同的重载形式。最简单的一个形式就是传入std::string 以及一个 std::regex 进行匹配,当匹配成功时,会返回 true,否则返回 false。
10. 语言级线程支持
std::thread
std::mutex/std::unique_lock
std::future/std::packaged_task
std::condition_variable
代码编译需要使用 -pthread 选项
11. 右值引用和move语义
先看一个简单的例子直观感受下:
string a(x); // line 1
string b(x + y); // line 2
string c(some_function_returning_a_string()); // line 3
如果使用以下拷贝构造函数:
string(const string& that)
{
size_t size = strlen(that.data) + 1;
data = new char[size];
memcpy(data, that.data, size);
}
以上3行中,只有第一行(line 1)的x深度拷贝是有必要的,因为我们可能会在后边用到x,x是一个左值(lvalues)。
第二行和第三行的参数则是右值,因为表达式产生的string对象是匿名对象,之后没有办法再使用了。
C++ 11引入了一种新的机制叫做“右值引用”,以便我们通过重载直接使用右值参数。我们所要做的就是写一个以右值引用为参数的构造函数:
string(string&& that) // string&& is an rvalue reference to a string
{
data = that.data;
that.data = 0;
}
我们没有深度拷贝堆内存中的数据,而是仅仅复制了指针,并把源对象的指针置空。事实上,我们“偷取”了属于源对象的内存数据。由于源对象是一个右值,不会再被使用,因此客户并不会觉察到源对象被改变了。在这里,我们并没有真正的复制,所以我们把这个构造函数叫做“转移构造函数”(move constructor),他的工作就是把资源从一个对象转移到另一个对象,而不是复制他们。
有了右值引用,再来看看赋值操作符:
string& operator=(string that)
{
std::swap(data, that.data);
return *this;
}
注意到我们是直接对参数that传值,所以that会像其他任何对象一样被初始化,那么确切的说,that是怎样被初始化的呢?对于C++ 98,答案是复制构造函数,但是对于C++ 11,编译器会依据参数是左值还是右值在复制构造函数和转移构造函数间进行选择。
如果是a=b,这样就会调用复制构造函数来初始化that(因为b是左值),赋值操作符会与新创建的对象交换数据,深度拷贝。这就是copy and swap 惯用法的定义:构造一个副本,与副本交换数据,并让副本在作用域内自动销毁。这里也一样。
如果是a = x + y,这样就会调用转移构造函数来初始化that(因为x+y是右值),所以这里没有深度拷贝,只有高效的数据转移。相对于参数,that依然是一个独立的对象,但是他的构造函数是无用的(trivial),因此堆中的数据没有必要复制,而仅仅是转移。没有必要复制他,因为x+y是右值,再次,从右值指向的对象中转移是没有问题的。
总结一下:复制构造函数执行的是深度拷贝,因为源对象本身必须不能被改变。而转移构造函数却可以复制指针,把源对象的指针置空,这种形式下,这是安全的,因为用户不可能再使用这个对象了。
下面我们进一步讨论右值引用和move语义。
C++98标准库中提供了一种唯一拥有性的智能指针std::auto_ptr,该类型在C++11中已被废弃,因为其“复制”行为是危险的。
auto_ptr<Shape> a(new Triangle);
auto_ptr<Shape> b(a);
注意b是怎样使用a进行初始化的,它不复制triangle,而是把triangle的所有权从a传递给了b,也可以说成“a 被转移进了b”或者“triangle被从a转移到了b”。
auto_ptr 的复制构造函数可能看起来像这样(简化):
auto_ptr(auto_ptr& source) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
auto_ptr 的危险之处在于看上去应该是复制,但实际上确是转移。调用被转移过的auto_ptr 的成员函数将会导致不可预知的后果。所以你必须非常谨慎的使用auto_ptr ,如果他被转移过。
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
显然,在持有auto_ptr 对象的a表达式和持有调用函数返回的auto_ptr值类型的make_triangle()表达式之间一定有一些潜在的区别,每调用一次后者就会创建一个新的auto_ptr对象。这里a 其实就是一个左值(lvalue)的例子,而make_triangle()就是右值(rvalue)的例子。
转移像a这样的左值是非常危险的,因为我们可能调用a的成员函数,这会导致不可预知的行为。另一方面,转移像make_triangle()这样的右值却是非常安全的,因为复制构造函数之后,我们不能再使用这个临时对象了,因为这个转移后的临时对象会在下一行之前销毁掉。
我们现在知道转移左值是十分危险的,但是转移右值却是很安全的。如果C++能从语言级别支持区分左值和右值参数,我就可以完全杜绝对左值转移,或者把转移左值在调用的时候暴露出来,以使我们不会不经意的转移左值。
C++ 11对这个问题的答案是右值引用。右值引用是针对右值的新的引用类型,语法是X&&。以前的老的引用类型X& 现在被称作左值引用。
使用右值引用X&&作为参数的最有用的函数之一就是转移构造函数X::X(X&& source),它的主要作用是把源对象的本地资源转移给当前对象。
C++ 11中,std::auto_ptr< T >已经被std::unique_ptr< T >所取代,后者就是利用的右值引用。
其转移构造函数:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
这个转移构造函数跟auto_ptr中复制构造函数做的事情一样,但是它却只能接受右值作为参数。
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
第二行不能编译通过,因为a是左值,但是参数unique_ptr&& source只能接受右值,这正是我们所需要的,杜绝危险的隐式转移。第三行编译没有问题,因为make_triangle()是右值,转移构造函数会将临时对象的所有权转移给对象c,这正是我们需要的。
转移左值
有时候,我们可能想转移左值,也就是说,有时候我们想让编译器把左值当作右值对待,以便能使用转移构造函数,即便这有点不安全。出于这个目的,C++ 11在标准库的头文件< utility >中提供了一个模板函数std::move。实际上,std::move仅仅是简单地将左值转换为右值,它本身并没有转移任何东西。它仅仅是让对象可以转移。
以下是如何正确的转移左值:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
请注意,第三行之后,a不再拥有Triangle对象。不过这没有关系,因为通过明确的写出std::move(a),我们很清楚我们的意图:亲爱的转移构造函数,你可以对a做任何想要做的事情来初始化c;我不再需要a了,对于a,您请自便。
当然,如果你在使用了mova(a)之后,还继续使用a,那无疑是搬起石头砸自己的脚,还是会导致严重的运行错误。
总之,std::move(some_lvalue)将左值转换为右值(可以理解为一种类型转换),使接下来的转移成为可能。
一个例子:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
上面的parameter,其类型是一个右值引用,只能说明parameter是指向右值的引用,而parameter本身是个左值。(Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.)
因此以上对parameter的转移是不允许的,需要使用std::move来显示转换成右值。
12. 智能指针
核心思想:为防止内存泄露等问题,用一个对象来管理野指针,使得在该对象构造时获得该指针管理权,析构时自动释放(delete).
基于此思想C++98提供了第一个智能指针:auto_ptr
auto_ptr基于所有权转移的语义,即将一个就的auto_ptr赋值给另外一个新的auto_ptr时,旧的那一个就不再拥有该指针的控制权(内部指针被赋值为null),那么这就会带来一些根本性的破绽:
- 函数参数传递时,会有隐式的赋值,那么原来的auto_ptr自动失去了控制权
- 自我赋值时,会将自己内部指针赋值为null,造成bug
因为auto_ptr的各种bug,C++11标准基本废弃了这种类型的智能指针,转而带来了三种全新的智能指针:
- shared_ptr,基于引用计数的智能指针,会统计当前有多少个对象同时拥有该内部指针;当引用计数降为0时,自动释放
- weak_ptr,基于引用计数的智能指针在面对循环引用的问题将无能为力,因此C++11还引入weak_ptr与之配套使用,weak_ptr只引用,不计数
- unique_ptr: 遵循独占语义的智能指针,在任何时间点,资源智能唯一地被一个unique_ptr所占有,当其离开作用域时自动析构。资源所有权的转移只能通过std::move()而不能通过赋值
需要记住的事,在以下三种情况下会引起引用计数的变更:
1.调用构造函数时: SmartPointer p(new Object());
2.赋值构造函数时: SmartPointer p(const SmartPointer &p);
3.赋值时:SmartPointer p1(new Object()); SmartPointer p2 = p1;