线上阅读链接:https://changkun.de/modern-cpp/zh-cn/00-preface/
最近正在阅读链接上这本《现代C++教程》,发帖作为学习笔记,如有侵权,告知即删。
第4章 容器
C++11之后扩充了提供的容器,包括线性容器(有序容器)、无序容器和元组,使C++能够更便捷、高效地实现一些功能,但是也仍有一些容器的功能非常有限,需要手动添加一些代码实现较为完整的功能。
线性容器,① 引入了std::array
容器,是实例化对象后大小固定的类型;② 引入了与std::list
类似的std::forward_list
,但基于单向链表实现,插入和搜索复杂度为
O
(
1
)
O(1)
O(1) 。
无序容器,想较传统C++的std::map
、std::set
这些容器,引入了不进行排序、通过Hash实现的std::unordered_map
、std::unordered_multimap
、std::unordered_set
和 std::unordered_multiset
两组无序容器,插入和搜索的平均复杂度为
O
(
1
)
O(1)
O(1) 。
元组,引入了std::tuple
以存放不限制数量的不同类型的数据,提供了构建、获取、拆包、合并等基本操作,而运行期的索引和元组遍历目前还没有现成的库函数实现,但是可以通过一些通用的模板函数代码实现,具体见后面的详细介绍。
4.1 线性容器
std::array
引入std::array
容器,其与std::vector
的区别在于,前者对象的大小是固定的,必须使用常量表达式定义数组大小,而后者可以自动扩容,并且不会自动归还被删除元素相应的内存,要手动运行 shrink_to_fit()
释放对应内存。
constexpr int len = 4;
std::array<int, len> arr = {1, 2, 3, 4};
std::array
的变量名无法隐式转换为元素类型的指针,而是需要通过一些其他方式:
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::forward_list】
列表容器 std::forward_list
的使用方法 std::list
基本类似,不同点是:
std::list
基于双向链表实现,而std::forward_list
使用单向链表实现,提供
O
(
1
)
O(1)
O(1) 复杂度的元素插入,不支持随机访问,不提供 size()
方法,不需要双向迭代时比 std::list
空间利用率更高。
4.2 无序容器
传统C++中有有序容器std::map
/std::set
,这些类型内部通过红黑树对元素进行比较和存储操作,插入和搜索的平均复杂度为
O
(
log
(
N
)
)
O(\log(N))
O(log(N)) ,对容器遍历时按照 <
顺序逐个遍历。
**无序容器**的元素不进行排序,而是通过Hash表实现,插入和搜索的平均复杂度为 $O(1)$ ,性能显著提升。
C++引入两组无序容器:std::unordered_map
、std::unordered_multimap
、std::unordered_set
和 std::unordered_multiset
,其中的 multi
表示该数据类型是否能够包含等值元素。
4.3 元组
传统C++容器中只有 std::pair
能够用来存放不同类型数据,但是又只能保存两个元素,由此想到了python中的元组类型,引入std::tuple
以存放不限制数量的不同类型数据。
元组基本操作:构建、获取、拆包
-
std::make_tuple
: 构造元组auto student = std::make_tuple(3.8, 'A', "张三"); // 或者 std::tuple<double, char, std::string> student(3.8, 'A', "张三");
-
std::get
: 获得元组某个位置的值cout << "姓名:" << std::get<2>(student) << endl; // C++14中还增加了使用类型来获取元组中的对象 std::tuple<std::string, double, double, int> t("123", 4.5, 6.7, 8); std::cout << std::get<std::string>(t) << std::endl; std::cout << std::get<double>(t) << std::endl; // 非法, 引发编译期错误 std::cout << std::get<3>(t) << std::endl;
-
std::tie
: 元组拆包double gpa; char grade; std::string name; std::tie(gpa, grade, name) = student;
案例程序
#include <tuple>
#include <iostream>
auto get_student(int id)
{
// 返回类型被推断为 std::tuple<double, char, std::string>
if (id == 0) return std::make_tuple(3.8, 'A', "张三");
if (id == 1) return std::make_tuple(2.9, 'C', "李四");
if (id == 2) return std::make_tuple(1.7, 'D', "王五");
return std::make_tuple(0.0, 'D', "null");
// 如果只写 0 会出现推断错误, 编译失败
}
int main()
{
auto student = get_student(0);
std::cout << "ID: 0, " << "GPA: " << std::get<0>(student) << ", "
<< "成绩: " << std::get<1>(student) << ", "
<< "姓名: " << std::get<2>(student) << '\n';
double gpa;
char grade;
std::string name;
// 元组进行拆包
std::tie(gpa, grade, name) = get_student(1);
std::cout << "ID: 1, " << "GPA: " << std::get<0>(student) << ", "
<< "成绩: " << std::get<1>(student) << ", "
<< "姓名: " << std::get<2>(student) << '\n';
}
运行期索引
std::get<>
依赖于一个编译期的常量,比如下面的代码就是不合法的:
int index = 1;
std::get<index>(t); // 非法
于是C++17引入了std::varient
,提供了 varient<>
类型模板参数
int i = 1;
std::cout << tuple_index(t, i) << std::endl;
注意,需要首先添加下面的代码才可以使用 tuple_index
函数
#include <variant>
template <size_t n, typename... T>
constexpr std::variant<T...> _tuple_index(const std::tuple<T...>& tpl, size_t i) {
if constexpr (n >= sizeof...(T))
throw std::out_of_range("越界.");
if (i == n)
return std::variant<T...>{ std::in_place_index<n>, std::get<n>(tpl) };
return _tuple_index<(n < sizeof...(T)-1 ? n+1 : 0)>(tpl, i);
}
template <typename... T>
constexpr std::variant<T...> tuple_index(const std::tuple<T...>& tpl, size_t i) {
return _tuple_index<0>(tpl, i);
}
template <typename T0, typename ... Ts>
std::ostream & operator<< (std::ostream & s, std::variant<T0, Ts...> const & v) {
std::visit([&](auto && x){ s << x;}, v);
return s;
}
元组合并与遍历
合并两个元组通过 std::tuple_cat
实现
auto new_tuple = std::tuple_cat(get_student(1), std::move(t));
而遍历一个元组可以先获取元组的长度,然后索引元组获取元素
template <typename T>
auto tuple_len(T &tpl) {
return std::tuple_size<T>::value;
}
for(int i = 0; i != tuple_len(new_tuple); ++i)
std::cout << tuple_index(new_tuple, i) << std::endl; // 运行期索引
注意,需要先调用下面的代码才可以调用tuple_len
函数
template <typename T>
auto tuple_len(T &tpl) {
return std::tuple_size<T>::value;
}
第5章 智能指针与内存管理
一般而言,对象在构造时申请空间,离开作用域或析构时释放空间,但是通过动态分配创建出来的对象,在传统C++中需要使用new
和delete
手动管理它们的创建和删除,但是一个对象创建出来之后,很可能被多个指针所指,如此一来,很难确定应该何时进行delete
操作。
由此,C++11引入了智能指针的概念,采用了引用计数的方法。
共享指针shared_ptr
,使用std::make_shared
创建,消除对new
的显式引用,当指向同一对象的共享指针数量变为零时,将对象自动删除;另外还提供了获取原始指针、减少引用计数、查看引用计数等操作的方法。
独占指针unique_ptr
,使用std::make_unique
创建,禁止其他智能指针与其共享同一对象,以确保代码安全,虽然不能复制,但是可以通过std::move
移动语义将对象转移给其他智能指针,在转移过程中不会发生类的构造和析构。
弱指针weak_ptr
,用以解决有了 shared_ptr
循环引用可能引发的内存泄露问题。
引言:关于引用计数
引用计数是一种为了防止内存泄露而产生的计数。
基本想法是:对动态分配的对象,① 每增加一次对同一个对象的引用,就增加一次引用对象的引用计数;② 每删除一次引用,引用计数就会减一;③ 当一个对象的引用计数减为零时,就自动删除其所指向的堆内存。
而在传统C++的实际实践中,很可能忘记释放资源而导致泄露。通常的做法是:(对一个对象)在构造函数时申请空间,在析构函数/离开函数作用域时释放空间,这就是 RAII 资源获取即初始化技术。
传统C++中,如果在自由存储上给对象分配空间,就需要使用 new
和 delete
并“记住”释放资源。C++11引入了基于引用计数想法的智能指针概念,包括std::shared_ptr
/std::unique_ptr
/std::weak_ptr
,使用时需包含头文件<memory>
,让程序员无需关心手动释放内存。
5.2 std::shared_ptr 共享指针
shared_ptr
能够记录有多少个 shared_ptr
共同指向一个对象,当引用计数变为零时将对象自动删除,避免了 delete
的显式调用。
但是使用 shared_ptr
仍需要使用 new
调用,std::make_shared
用来消除对 new
的显式使用,自动返回所传入参数的对象的类型的 std::shared_ptr
指针。
#include <iostream>
#include <memory>
void foo(std::shared_ptr<int> i) {
(*i)++;
}
int main() {
// auto pointer = new int(10); // illegal, no direct assignment
// Constructed a std::shared_ptr
auto pointer = std::make_shared<int>(10);
foo(pointer);
std::cout << *pointer << std::endl; // 11
// The shared_ptr will be destructed before leaving the scope
return 0;
}
std::shared_ptr
提供了获取原始指针get()
、减少引用计数reset()
、查看引用计数use_count()
等操作的方法。注意,这里的 reset
是由某一具体的 shared_ptr
执行,执行后该智能指针不能在获取到原始指针和值,其他指向同一指针的智能指针的引用计数减一,并且任能获取、访问原指针。
auto pointer = std::make_shared<int>(10);
auto pointer2 = pointer; // 引用计数+1
auto pointer3 = pointer; // 引用计数+1
int *p = pointer.get(); // 这样不会增加引用计数
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 3
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 3
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 3
pointer2.reset();
std::cout << "reset pointer2:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 2
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // pointer2 has been reset; 0
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 2
pointer3.reset();
std::cout << "reset pointer3:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 1
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // pointer3 has been reset; 0
5.3 std::unique_ptr 独占指针
std::unique_ptr
是一种独占的智能指针,禁止其他智能指针与其共享同一个对象,以保证代码安全,创建方式与前面的共享指针类似,是std::make_unique<>()
,但是该方法是C++14才被加入了(C++11时被落了),可以使用下面的代码自行实现。
std::unique_ptr<int> pointer = std::make_unique<int>(10); // make_unique 从 C++14 引入
std::unique_ptr<int> pointer2 = pointer; // 非法
自行实现std::make_unique
template<typename T, typename ...Args>
std::unique_ptr<T> make_unique( Args&& ...args ) {
return std::unique_ptr<T>( new T( std::forward<Args>(args)... ) );
}
虽然独占指针不能复制,但是可以利用 std::move
将其转移给其他的 unique_ptr
。
#include <iostream>
#include <memory>
struct Foo {
Foo() { std::cout << "Foo::Foo" << std::endl; }
~Foo() { std::cout << "Foo::~Foo" << std::endl; }
void foo() { std::cout << "Foo::foo" << std::endl; }
};
void f(const Foo &) {
std::cout << "f(const Foo&)" << std::endl;
}
int main() {
std::unique_ptr<Foo> p1(std::make_unique<Foo>());
if (p1) p1->foo(); // p1 不空, 输出
{
std::unique_ptr<Foo> p2(std::move(p1)); // 此时不会再调用构造函数
f(*p2); // p2 不空, 输出
if(p2) p2->foo();// p2 不空, 输出
if(p1) p1->foo();// p1 为空, 无输出
p1 = std::move(p2);
if(p2) p2->foo(); // p2 为空, 无输出
std::cout << "p2 被销毁" << std::endl; // p2本身已经是空指针,因此不会调用析构函数
}
if (p1) p1->foo(); // p1 不空, 输出
// Foo 的实例会在离开作用域时被销毁
}
5.4 【std::weak_ptr 弱指针】
仔细思考 std::shared_ptr
仍然存在资源无法释放的问题,比如下面的代码所展示的
struct A;
struct B;
struct A {
std::shared_ptr<B> pointer;
~A() {
std::cout << "A 被销毁" << std::endl;
}
};
struct B {
std::shared_ptr<A> pointer;
~B() {
std::cout << "B 被销毁" << std::endl;
}
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->pointer = b;
b->pointer = a;
}
主函数中创建的智能指针分别指向 A
和 B
,且 A
和 B
内部各有一个共享指针指向彼此,由此, A
和 B
的共享指针的引用数均是2,当主函数运行结束,只分别销毁了一个共享指针,引用数没有清零,而外部已无法找到这个区域,造成了内存泄露,过程如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/bf97b59159eb10ff43468a9ec323b0bb.png)
弱引用不会引起引用计数的增加,std::weak_ptr
没有 *
和 ->
运算符,因此不能对资源进行操作,可以:
① 用其 expired()
方法检查共享指针 weak_ptr
是否存在。
② 用其 lock()
方法在原始对象未被释放时返回一个指向原始对象的std::shared_ptr
,进而访问原始对象资源,如果已没有指向原始对象资源的共享指针,则返回nullptr
。