C++11 如此受欢迎,你都用过哪些新特性?(二)

随着 C++ 的现代化进程在加快,前段时间有些朋友和我提起,现在的 C++ 语法他/她已经看不懂了。有必要让初进 C++ 的朋友对 C++ 语言的现代化版本有更全面的认识,藉此机会,我们就一起聊聊现代化的起点版本 C++ 11 引入了哪些比较有用的新特性?

cpp11new.jpeg
接前文~

上一篇聊的是 C++11 语法相关的新特性,这里将要聊的是 C++11 标准库提供的新特性。

智能指针 (std::unique_ptrstd::shared_ptr)

C++11 之前,开发调试中经常会碰到的大问题是内存指针安全,经常操作的裸指针可能是空指针,就像用手在把玩一颗定时炸弹。为了避免直接操作裸指针,也为了贯彻 RAII 的设计原则,智能指针出现了。

内存被分配之后,交给智能指针托管,内存的生命周期就完全由智能指针管理了,用户无需手动释放内存,就像有个隐形的垃圾回收器在时刻监视中。

智能指针是模板类,可以管理各种类型的内存对象。C++11 提供了两种智能指针实现,分别是 std::unique_ptrstd::shared_ptr

std::unique_ptrstd::shared_ptr 的区别是,同一个内存对象不能同时被多个 std::unique_ptr 管理,而 std::shared_ptr 可以同时管理多个内存对象。

来看一下例子:

#include <iostream>
#include <memory>

void printSharedPtrInfo(const std::shared_ptr<int>& sp,
                        const char* name) {
    std::cout << name << ": " << *sp
    << ", use_count: " << sp.use_count()
    << std::endl;
}

int main() {
    // unique_ptr
    std::unique_ptr<int> ptr1(new int(5));
    std::unique_ptr<int> ptr2 = std::move(ptr1);
    if (!ptr1) {
        std::cout << "ptr1 is empty." << std::endl;
    }
    if (ptr2) {
        std::cout << "Value of ptr2: " << *ptr2 << std::endl;
    }

    // shared_ptr
    auto sp1 = std::make_shared<int>(6);
    printSharedPtrInfo(sp1, "sp1");
    {
        std::shared_ptr<int> sp2 = sp1;
        printSharedPtrInfo(sp2, "sp2");
        printSharedPtrInfo(sp1, "sp1");
    }
    printSharedPtrInfo(sp1, "sp1");

    return 0;
}

同一个内存对象不能被不同的 std::unique_ptr 托管,也就是共享,但是可以在不同的 std::unique_ptr 之间移动。std::move 用于将对象转换为可移动的右值,方便调用 std::unique_ptr 的移动赋值操作符或移动构造函数实现内存对象的移动。

不同的 std::shared_ptr 对同一个内存对象的共享,得益于该摸版类内部的计数器,通过方法 use_count() 可获取计数值。某内存对象正被有 n 个 std::shared_ptr 托管,则计数值为 n。

智能指针对象离开自己的生命周期后,如果计数器归零,会自动释放被托管的内存对象。

std::unique_ptr 的计数器不能大于 1,体现独享属性,而 std::shared_ptr 可以大于 1,体现共享属性。

八戒在之前的文章中有详细介绍过 RAII 设计原则,文末有推荐阅读连接,不要错过!

std::move

上面的例子中提到了 std::move 的使用是为了将对象转换为可移动的右值,那么为什么转换为右值?

右值的意义是避免复制数据过程中的无意义消耗,一个对象从一个位置挪到另一个位置,总会比挪动副本要高效,因为亲自下场往往能救火。

std::vector<std::string> v;
std::string s = "hello";
v.push_back(std::move(s));

那么为了移动数据就必须多此一举,显式引用 std::move 吗?

显式引用 std::move 是为了明确告诉编译器,使用者的目标是移动而不是复制,避免隐式转换,隐私转换可能不符合使用者的意图。

因为通常的类定义里,既可以定义拷贝构造、拷贝赋值操作符,也可以定义移动构造、移动赋值操作符。没有明确目的,就要看编译器人品了。

std::thread 和多线程库

在 C++11 之前,想要创建子线程需要借用系统 API 或者第三方库 Boost 等,是一件很麻烦的事,所以万众期待的线程库在 C++11 中闪亮登场。

从此,开发多线程代码可以跨平台了,阅读负担也降低了很多。

std::thread 是 C++11 提供的核心线程管理类,用于创建和管理线程,创建子线程时可直接传入执行函数。

#include <iostream>
#include <thread>

void fun() {
    std::cout << "Hello std::thread!" << std::endl;
}

int main() {
    std::thread t(fun);
    t.join();
    return 0;
}

std::thread 创建子线程对象后,子线程默认允许自动执行。通过子线程对象还可以管理线程的执行流程,std::thread::join() 阻塞当前线程并等待子线程执行完毕再返回。

std::unordered_map 和其他哈希容器

在访问大型数据集时,为了高效查找元素,常常使用容器 std::mapstd::setstd::map 是面向于有序的键值对,std::set 则用于有序的不重复数据集。

std::mapstd::set 的底层实现都是基于平衡二叉树算法,所以时间复杂度是 O(log n)。随着数据集的继续膨胀,数据访问的效率会缓慢下降,下降的幅度取决于数据量的大小。

为了应对超大型数据集,进一步提高访问的效率,同时提供无序的数据容器,C++11 引入了哈希容器,比如 std::unordered_mapstd::unordered_set 等等哈希容器。

#include <iostream>
#include <unordered_map>
#include <string>

int main() {
    std::unordered_map<std::string, int> ageMap;

    ageMap["A"] = 30;
    ageMap["B"] = 25;
    ageMap["C"] = 35;

    if (ageMap.find("A") != ageMap.end()) {
        std::cout << "A's age is "
            << ageMap["A"] << std::endl;
    }

    for (const auto& pair : ageMap) {
        std::cout << pair.first << ": "
            << pair.second << std::endl;
    }

    ageMap.erase("B");

    if (ageMap.count("B") == 0) {
        std::cout << "B is not in the map."
            << std::endl;
    }

    return 0;
}

哈希容器还允许自定义哈希函数和等价比较函数,满足更多需求。

std::tuple

对于固定长度的数组,要求每个元素的类型都必须一致。而动态数组 std::vector 虽然长度可以按需自动增长,但是元素的数据类型也必须一致。为了满足数组内部各个元素类型多样化的需求,C++11 引入了 std::tuple

std::tuple 是容器,用于存储固定数量不同类型的元素,有点类似结构体,但它的成员类型不必相同,也不需要为每个成员命名。

#include <iostream>
#include <tuple>

int main() {
    std::tuple<int, double, std::string> myTuple(2, 5, "hi");

    int value1 = std::get<0>(myTuple);
    double value2 = std::get<1>(myTuple);
    std::string value3 = std::get<2>(myTuple);

    std::cout << "Tuple contents: " << value1
        << ", " << value2 << ", " << value3
        << std::endl;

    int a;
    double b;
    std::string c;
    std::tie(a, b, c) = myTuple;
    std::cout << "Unpacked tuple: " << a
    << ", " << b << ", " << c << std::endl;

    return 0;
}

std::get<i> 用于访问索引 i 对应的 std::tuple 元素,std::tie 帮助实现对 std::tuple 容器的解包。

std::array

传统的数组在使用过程中,经常会碰到一个很致命的问题,访问索引越界,进而引发错误。为了协助开发者更好地规避索引越界的问题,C++11 还引入了 std::array

std::array 属于容器,它的长度在初始化后就是固定的了,元素类型也必须一致,可以认为是升级版的数组,通过成员方法 at() 访问元素时自带边界检查,完美替代传统数组。

#include <iostream>
#include <array>
#include <algorithm>

int main() {
    std::array<int, 5> myArray = {1, 2, 3, 4, 5};

    std::cout << "First element: "
        << myArray[0] << std::endl;
    std::cout << "Last element: "
        << myArray.back() << std::endl;

    for (auto it = myArray.begin();
        it != myArray.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    for (const auto& elem : myArray) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    std::reverse(myArray.begin(), myArray.end());

    // 输出反转后的 array
    for (const auto& elem : myArray) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}

由于 std::array 是容器实现,所以新版的范围 for 循环语句简化了数组的迭代访问。即使过去对数组元素顺序反转的费时麻烦操作,在 std::array 这里也可以轻松通过 std::reverse() 实现。

std::chrono 时间库

传统的 C++ 里对时间的操作都是基于 C 标准库里 time.h 提供的 time()、clock() 等,无论是从数据精度还是功能丰富度来看都很欠缺,甚至还有的要依赖系统的特定 API。

随着应用的需求扩增,对时间的处理要求越来越多,势必要求 C++ 提供更强的时间处理能力。

C++11 引入了 std::chrono 时间库,可以输出更高精度的时间数据,要求不同单位的时间必须显示转换避免隐式错误,功能上提供了时间点和时间间隔的丰富操作,更关键的是可以跨平台。

来看一下不同时间精度的处理:

using namespace std::chrono;
auto start = high_resolution_clock::now();
// ...
auto end = high_resolution_clock::now();
auto duration = duration_cast<milliseconds>(end - start);
std::cout << "Elapsed time: " << duration.count() << " ms\n";

时间单位转换:

auto ms = milliseconds(1000);
auto sec = duration_cast<seconds>(ms);

显示的单位转换:

using namespace std::chrono;
auto ms = milliseconds(1000);
auto sec = duration_cast<seconds>(ms);