C++11 如此受欢迎,你都用过哪些新特性?(二)
随着 C++ 的现代化进程在加快,前段时间有些朋友和我提起,现在的 C++ 语法他/她已经看不懂了。有必要让初进 C++ 的朋友对 C++ 语言的现代化版本有更全面的认识,藉此机会,我们就一起聊聊现代化的起点版本 C++ 11 引入了哪些比较有用的新特性?
接前文~
上一篇聊的是 C++11 语法相关的新特性,这里将要聊的是 C++11 标准库提供的新特性。
智能指针 (std::unique_ptr
和 std::shared_ptr
)
C++11 之前,开发调试中经常会碰到的大问题是内存指针安全,经常操作的裸指针可能是空指针,就像用手在把玩一颗定时炸弹。为了避免直接操作裸指针,也为了贯彻 RAII 的设计原则,智能指针出现了。
内存被分配之后,交给智能指针托管,内存的生命周期就完全由智能指针管理了,用户无需手动释放内存,就像有个隐形的垃圾回收器在时刻监视中。
智能指针是模板类,可以管理各种类型的内存对象。C++11 提供了两种智能指针实现,分别是 std::unique_ptr
和 std::shared_ptr
。
std::unique_ptr
和 std::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::map
和 std::set
。std::map
是面向于有序的键值对,std::set
则用于有序的不重复数据集。
std::map
和 std::set
的底层实现都是基于平衡二叉树算法,所以时间复杂度是 O(log n)。随着数据集的继续膨胀,数据访问的效率会缓慢下降,下降的幅度取决于数据量的大小。
为了应对超大型数据集,进一步提高访问的效率,同时提供无序的数据容器,C++11 引入了哈希容器,比如 std::unordered_map
、std::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);