C++11 新特性学习 ——《现代C++教程》阅读笔记:第4、5章-容器、智能指针与内存管理

线上阅读链接: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::mapstd::set这些容器,引入了不进行排序、通过Hash实现std::unordered_mapstd::unordered_multimapstd::unordered_setstd::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_mapstd::unordered_multimapstd::unordered_setstd::unordered_multiset ,其中的 multi 表示该数据类型是否能够包含等值元素。

4.3 元组

传统C++容器中只有 std::pair 能够用来存放不同类型数据,但是又只能保存两个元素,由此想到了python中的元组类型,引入std::tuple 以存放不限制数量的不同类型数据。

元组基本操作:构建、获取、拆包

  1. std::make_tuple: 构造元组

    auto student = std::make_tuple(3.8, 'A', "张三");
    // 或者
    std::tuple<double, char, std::string> student(3.8, 'A', "张三");
    
  2. 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;
    
  3. 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++中需要使用newdelete手动管理它们的创建和删除,但是一个对象创建出来之后,很可能被多个指针所指,如此一来,很难确定应该何时进行delete操作。

​ 由此,C++11引入了智能指针的概念,采用了引用计数的方法。

共享指针shared_ptr,使用std::make_shared创建,消除对new的显式引用,当指向同一对象的共享指针数量变为零时,将对象自动删除;另外还提供了获取原始指针、减少引用计数、查看引用计数等操作的方法。

独占指针unique_ptr,使用std::make_unique创建,禁止其他智能指针与其共享同一对象,以确保代码安全,虽然不能复制,但是可以通过std::move移动语义将对象转移给其他智能指针,在转移过程中不会发生类的构造和析构。

弱指针weak_ptr,用以解决有了 shared_ptr 循环引用可能引发的内存泄露问题。

引言:关于引用计数

引用计数是一种为了防止内存泄露而产生的计数。

​ 基本想法是:对动态分配的对象,① 每增加一次对同一个对象的引用,就增加一次引用对象的引用计数;② 每删除一次引用,引用计数就会减一;③ 当一个对象的引用计数减为零时,就自动删除其所指向的堆内存

​ 而在传统C++的实际实践中,很可能忘记释放资源而导致泄露。通常的做法是:(对一个对象)在构造函数时申请空间,在析构函数/离开函数作用域时释放空间,这就是 RAII 资源获取即初始化技术

​ 传统C++中,如果在自由存储上给对象分配空间,就需要使用 newdelete 并“记住”释放资源。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;
}

主函数中创建的智能指针分别指向 AB ,且 AB 内部各有一个共享指针指向彼此,由此, AB 的共享指针的引用数均是2,当主函数运行结束,只分别销毁了一个共享指针,引用数没有清零,而外部已无法找到这个区域,造成了内存泄露,过程如下图所示:

弱引用不会引起引用计数的增加,std::weak_ptr 没有 *-> 运算符,因此不能对资源进行操作,可以:

① 用其 expired() 方法检查共享指针 weak_ptr 是否存在。

② 用其 lock() 方法在原始对象未被释放时返回一个指向原始对象的std::shared_ptr,进而访问原始对象资源,如果已没有指向原始对象资源的共享指针,则返回nullptr

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值