首页 C++ C++11 的重大改变

C++11 的重大改变

7 2.4K

原文出处:http://blog.smartbear.com/software-quality/bid/167271/The-Biggest-Changes-in-C-11-and-Why-You-Should-Care

自从 C++ 语言第一次迭代已经过去 13 年。C++ 标准委员会成员 Danny Kalev 在本文中解释了这门编程语言有怎样的改进,以及如何帮助你编写更好的代码。

C++ 的发明者 Bjarne Stroustrup 最近说,C++11 “感觉就像是一个新的语言——各部分之间能够更好的协作”。事实上,核心 C++11 已经有了很大的改变。现在它支持 lambda 表达式,自动类型推断,统一的初始化语法,委托构造函数,已删除和默认函数声明,nullptr,以及最重要的,右值引用——一种预言将会改变创造和处理对象方法的技术。下面我们将一一说明。

C++11 标准库同样增加了新的算法,新的容器类,原子运算,类型特性,正则表达式,新的智能指针,async()能力以及多线程库。

C++11 的完整特性以及库的列表可以在这里找到。

1998年 C++ 标准颁布之后,委员会中两个成员预言,下一个 C++ 标准“肯定”包含一个内建的垃圾回收器(GC),可能不会支持多线程,因为构建一个可移植的线程模型需要复杂的技术。13年之后,新的 C++ 标准,C++11 已经完成。猜猜发生了什么?这个标准缺少了 GC,却包含了一个现代化的线程库。

在这篇文章中,我将解释这门语言的重大改变,以及为什么把它们看出是一件大事。就像你即将看到的那样,线程库不是唯一的改变。 新的标准建立在数十年的专家建议基础之上,让 C++ 变得更加有意义。正如 Rogers Cadenhead 指出的那样,“这就像迪斯科、Pet Rocks(一种古老的美国游戏)和大龄的奥林匹克游泳运动员一样,C++ 表现得相当令人惊喜。”

首先我们来看看 C++11 突出的核心语言特性。

Lambda 表达式

lambda 表达式允许你定义本地(也就是调用函数的地方)函数,因此可以排除掉许多函数对象导致的冗余以及安全风险。lambda 表达式格式为:

[capture](parameters)->return-type {body}

在一个函数调用的参数列表中出现 [] 意味着开始一个 lambda 表达式。下面看一个 lambda 例子。

假设你想要计算一个字符串中有多少大写字母。使用for_each()遍历字符数组,下面的 lambda 表达式判断每一个字母是不是大写的。每找到一个大写字母,该 lambda 表达式将定义在表达式外的 Uppercase 变量的数值增加:

int main()
{
    char s[] = "Hello World!";
    int Uppercase = 0; // lambda 对其进行修改
    for_each(s, s+sizeof(s), [&Uppercase] (char c) {
        if (isupper(c))
        Uppercase++;
    });
    cout << Uppercase << " uppercase letters in: " << s << endl;
}

这就像你定义了一个函数,而函数体位于另外一个函数调用中。[&Uppercase]中的 & 意味着 lambda 体以引用方式获得 Uppercase 所以能够修改它。没有 & 符号,Uppercase 将以传值的方式传入。C++11 lambda 也包含了对成员函数的构造。

自动类型推断和 decltype

在 C++03 中,你必须在声明时指定对象类型。但是在许多情况下,对象的声明包含了一个初始化器。C++11 利用这一地点,允许你以不指定类型的方式声明对象:

auto x = 0; // 因为 0 是 int,所以 x 类型是 int
auto c = 'a'; // char
auto d = 0.5; // double
auto national_debt = 14400000000000LL; //long long

当对象类型太冗长或者是模板中的自动生成时,自动类型推断尤其有用。考虑:

void func(const vector<int> &vi)
{
    vector<int>::const_iterator ci = vi.begin();
}

你可以像下面一样声明这个遍历器:

auto ci = vi.begin();

auto关键字不是新增的,早在 ANSI C 之前就已经存在。但是 C++11 改变了其含义:auto不再意味着一个具有自动存储类型的对象。它意味着一个从其初始化器推断类型的对象。auto旧的语义已经从 C++11 移除,以避免产生歧义。

C++11 为捕获对象或表达式的类型提供了类似的机制。新的运算符decltype接收一个表达式并“返回”其类型:

const vector<int> vi;
typedef decltype (vi.begin()) CIT;
CIT another_const_iterator;

统一初始化语法

C++ 至少有四种不同的初始化方法,有些是重叠的。

带有括号的初始化类似:

std::string s("hello");
int m = int(); // 默认初始化

在某些特殊情况,你也可以使用 = 达到相同目的:

std::string s = "hello";
int x = 5;

对于 POD(Plain Old Data,具有 C 兼容特点)聚合,可以使用大括号:

int arr[4] = {0,1,2,3};
struct tm today = {0};

最后,在构造函数中使用成员初始化器:

struct S
{
    int x;
    S(): x(0) {}
};

初始化操作的多种变体是令人感觉困扰的重要原因之一,不仅仅对于新手而言。更糟糕的是,在 C++03 你不能对使用new[]分配空间的 POD 数组进行数组成员和 POD 本身的初始化。C++11 使用统一的大括号标记清除了这种混乱:

class C
{
    int a;
    int b;
public:
    C(int i, int j);
};

C c {0,0}; // C++11 可用,等价于 C c(0,0);

int* a = new int[3] { 1, 2, 0 }; // C++11 可用

class X
{
    int a[4];
public:
    X() : a{1,2,3,4} {} // C++11 可用,成员数组初始化器
};

对于容器,现在可以跟一长串push_back()调用说再见了。在 C++11 你可以直观地初始化容器:

// C++11 容器初始化器
vector<string> vs = { "first", "second", "third"};
map singers = { {"Lady Gaga", "+1 (212) 555-7890"},
                {"Beyonce Knowles", "+1 (212) 555-0987"}};

类似的,C++11 支持数据成员的类内初始化:

class C
{
    int a = 7; // C++11 可用
public:
    C();
};

删除和默认函数

具备如下形式的函数成为默认函数(defaulted function):

struct A
{
    A() = default; // C++11
    virtual ~A() = default; // C++11
};

=default;部分说明,编译器生成该函数的一个默认实现。默认函数有两个优势:比手工实现更有效;把程序员从手动定义这些函数的繁琐中解放出来。

与默认函数相反的是删除函数:

int func() = delete;

删除函数对防止对象复制很有用。回忆一下,C++ 为每个类自动声明一个拷贝构造函数和赋值运算符。为禁止复制,需要将这两个特殊的成员函数声明为=delete

struct NoCopy
{
    NoCopy & operator =( const NoCopy & ) = delete;
    NoCopy ( const NoCopy & ) = delete;
};
NoCopy a;
NoCopy b(a); // 编译错误,拷贝构造函数已经被删除

nullptr

终于,C++ 有了一个代表空指针常量的关键字。nullptr替换了充满 bug 的 NULL 宏,以及被用于空指针好多年的字面常量 0。nullptr是强类型的:

void f(int); // #1
void f(char *);// #2
// C++03
f(0); // 调用哪一个 f?
// C++11
f(nullptr) // 无歧义,调用 #2

nullptr适用于所有指针类型,包括函数指针和成员指针:

const char *pc = str.c_str(); // 数据指针
if (pc != nullptr)
    cout << pc << endl;
int (A::*pmf)() = nullptr; // 成员函数指针
void (*pmf)() = nullptr; // 函数指针

委托构造函数

在 C++11 中,构造函数可以调用同一个类中另外的构造函数:

class M // C++11 委托构造函数
{
    int x, y;
    char *p;
public:
    M(int v) : x(v), y(0),  p(new char [MAX])  {} // #1 目标
    M(): M(0) {cout << "delegating ctor" << endl;} // #2 委托
};

构造函数 #2 是委托构造函数,调用了目标构造函数 #1。

右值引用

C++03 的引用类型只能绑定左值。C++11 引入了新的引用类型——右值引用。右值引用可以绑定右值,例如临时变量和字面常量。

增加右值引用的主要原因是移动语义。不同于传统的拷贝,移动的含义是目标对象占有源对象的资源,将源对象设置为“空”状态。在这种情景下,拷贝一个对象既昂贵又不必要,应该使用移动操作符。为感受移动语义在性能上的优势,考虑交换字符串。一个原始的实现类似于:

void naiveswap(string &a, string & b)
{
    string temp = a;
    a = b;
    b = temp;
}

这种实现很昂贵。拷贝字符串需要分配内存,将字符从源对象复制到目标对象。相比而言,移动字符串仅仅意味着交换两个数据成员,不需要分配内存、复制字符数组和释放内存:

void moveswapstr(string& empty, string & filled)
{
    // 伪代码,体会思想
    size_t sz = empty.size();
    const char *p = empty.data();
    // 移动 filled 的资源到 empty
    empty.setsize(filled.size());
    empty.setdata(filled.data());
    // filled 编程空的了
    filled.setsize(sz);
    filled.setdata(p);
}

如果你正在实现一个支持移动的类,需要声明一个移动构造函数和移动复制运算符:

class Movable
{
    Movable (Movable&&); // 移动构造函数
    Movable&& operator=(Movable&&); // 移动复制运算符
};

C++11 标准库大量使用了移动语义。许多算法和容易也为移动语义做了优化。

C++11 标准库

2003年,C++ 以库技术报告 1(TR1)的形式经历了一次大型重构。TR1 包含了新的容器类(unordered_setunordered_mapunordered_multisetunordered_multimap)和许多新的库,例如正则表达式,元组,函数对象包装器。随着 C++11 的颁布,TR1 连同新的库一起正式集成到 C++ 标准中。下面是 C++11 标准库的特性:

线程库

毫无疑问,从程序员角度看,C++11 最重要的改进就是并发。C++11 有一个thread类,描述一个执行线程、promisefuture(用于并发环境下同步的对象),用于发起并发任务的模板函数 async() 和用于声明线程独立的数据的存储类型 thread_local。快速了解 C++11 线程库,请阅读 Anthony Williams 的文章 Simpler Multithreading in C++0x

新的智能指针类

C++98 只定义了一个智能指针类,auto_ptr,而这个类现在已经被废弃了。C++11 包含了新的智能指针类:shared_ptr 和最近新加的 unique_ptr。这两个类都与其他标准库组件兼容,所以你可以安全地将这些智能指针添加到标准容易以及使用标准算法操作。

新的算法

C++11 标准库定义了模拟集合论操作的新的算法all_of()any_of()none_of()。下面几行将谓词ispositive()应用于范围[first, first+n),然后使用all_of()any_of()none_of()检测范围的属性:

#include <algorithm>

// C++11 代码
// 所有元素都是正数吗?
all_of(first, first+n, ispositive()); // false
// 至少有一个元素是正数吗?
any_of(first, first+n, ispositive()); // true
// 没有元素是正数?
none_of(first, first+n, ispositive()); // false

还有一个新的copy_n算法。使用copy_n()将一个有 5 个元素的数组复制到另外一个可说是小菜一碟:

#include <algorithm>
int source[5] = { 0, 12, 34, 50, 80 };
int target[5];
// 从源数组到目的数组拷贝 5 个元素
copy_n(source, 5, target);

iota()算法创建一个递增的数字范围,就像首先给*first赋初始值,然后使用++递增。在下面的代码中,iota()将连续数值{10,11,12,13,14}赋值给数组arr,将{'a', 'b', 'c'}赋值给字符数组c

include <numeric>
int a[5] = {0};
char c[3] = {0};
iota(a, a+5, 10); // 将 a 修改为 { 10, 11, 12, 13, 14 }
iota(c, c+3, 'a'); // {'a', 'b', 'c'}

C++11 依然缺少一些有用的库,例如 XML API,socket,GUI,反射——当然,还是有合理的自动垃圾回收器。但是,现在 C++ 的确已经提供了很多新的特性,使得代码更加安全、高效(使得,迄今为止最高效的。参加 Google 的 benchmark tests)以及学习和使用起来更加简单。

如果 C++11 的改变过于宏大,不要抱怨。花些时间循序渐进地理解这些变化。在这一过程的最后,你可能就同意 Stroustrup 的意见:C++11 的确像是一种新的语言——一种更好的语言!

7 评论

KOFLazycat 2012年11月9日 - 16:11

楼主关于QT的技术交流群号是多少?前两个都满了进不去, 😆

回复
DevBean 2012年11月9日 - 23:21

前面两个应该没有满的啊…

回复
David 2015年2月4日 - 21:47

赞赞赞赞,看到的太晚了

回复
tsingkong 2021年4月28日 - 10:06

两个成员语言 -> 预言

回复
tsingkong 2021年4月28日 - 10:26

auto不在意味着 -> 不再
使用统一的大括号标记清楚了这种混乱 -> 清除

回复
豆子 2021年6月19日 - 16:48

谢谢指出,已修改

回复
a 2022年10月23日 - 12:26

元祖 -> 元组

回复

回复 a 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2