本文属于「现代C++学习实践」系列文章之一,这一系列正式开始于2021/09/04,着重于现代C++(即C++11、14、17、20、23等新标准)和Linux C++服务端开发的学习与实践。众所周知,「C++难就难在:在C++中你找不到任何一件简单的事」。因此,本系列将至少持续到作者本人「精通C++」为止(笑)。由于文章内容随时可能发生更新变动,欢迎关注和收藏现代C++系列文章汇总目录一文以作备忘。
为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/Modern-Cxx-Learning-Path。在这一仓库中,你可以看到本人学习C++的全过程,包括C++书籍源码、练习实现、小型项目等。
需要特别说明的是,为了透彻理解和全面掌握现代C++,本系列文章中参考了诸多博客、教程、文档、书籍等资料,限于时间精力有限,这里无法一一列出。部分重要资料的不完全参考目录如下所示,在后续学习整理中还会逐渐补充:
- C++ Primer 中文版(第5版),Stanley B. Lippman、Barbara E. Moo等著,王刚、杨巨峰译,叶劲峰、李云、刘未鹏等审校,电子工业出版社;
- Bjarne Stroustrup老爷子的个人网站。包括Thriving in a Crowded and Changing World: C++ 2006–2020及其中文版——在拥挤和变化的世界中茁壮成长:C++ 2006–2020(一份了解标准化背后故事、以及C++未来发展方向的绝佳材料)
- 侯捷老师的公开课;
- C++面向对象高级开发上、下:正确理解面向对象的精神和实现手法,涵盖对象模型、关键机制、编程风格、动态分配;
- STL标准库与范型编程:深入剖析STL标准库之六大部件、及其之间的体系结构,并分析其源码,引导高阶泛型编程。
- C++新标准C++11/14:在短时间内深刻理解C++2.0的诸多新特性,涵盖语言和标准库两层
- C++内存管理机制:学习由语言基本构件到高级分配器的设计与实作,并探及最低阶
malloc
的内部实现。- C++ Startup揭密:C++程序的生前和死后。认识Windows平台下的Startup Code(启动码),完全通透C++程序的整个运行过程。
目前为止,我们编写的程序中所使用的对象,都有着严格定义的生存期:
- 全局对象在程序启动时分配,在程序结束时销毁;
- 对于局部
static
对象,在第一次使用前分配,在程序结束时销毁; - 对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。
对应地,目前为止,我们的程序只使用过静态内存和栈内存:
- 静态内存用来保存局部
static
对象(参见6.6.1节,第185页)、类static
数据成员(参见7.6节,第268页)以及定义在任何函数之外的变量(即全局对象)。 - 栈内存用来保存定义在函数内的非
static
对象(即局部自动对象)。
分配在静态或栈内存中的对象,由编译器自动创建和销毁:
static
对象在使用之前分配,在程序结束时销毁;- 对于栈对象,仅在其定义的程序块运行时才存在。
除了 static
和自动对象外,C++还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。
对应地,除了静态内存和栈内存外,每个程序还拥有一个内存池,这部分内存被称作自由空间 free store
或堆 heap
。程序用堆来存储动态分配 dynamically allocate
的对象——即那些在程序运行时分配的对象。动态对象的生存期由程序来控制,当动态对象不被使用时,我们的代码必须显式地销毁它们。
然而,动态对象的正确释放,被证明是编程中极其容易出错的地方。为了更安全地使用动态对象,标准库定义了两个智能指针类型,以管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。
1. 动态内存与智能指针
在C++中,动态内存的管理是通过一对运算符来完成的:
new
,在动态内存中为对象分配空间,并返回一个指向该对象的指针;delete
,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。有时我们会忘记释放内存,这种情况下就会产生内存泄露;有时在尚有指针引用内存的情况下,我们就释放了它,这种情况下就会产生引用非法内存的指针。
为了更容易、更安全地使用动态内存,C++11标准库提供了两种智能指针 smart pointer
类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr
允许多个指针指向同一个对象;unique_ptr
则独占所指向的对象。标准库还定义了一个名为 weak_ptr
的伴随类,它是一种弱引用,指向 shared_ptr
所管理的对象。这三种类型都定义在 memory
头文件中。
1.1 shared_ptr
类
类似 vector
,智能指针也是类模板(参见3.3节,第86页)。因此创建一个智能指针时,编译器无法像使用函数模板一样从函数实参推断模板实参,我们必须提供额外的信息——指针可以指向的类型。与 vector
一样,我们在尖括号内给出类型,之后是所定义的这个智能指针的名字:
shared_ptr<string> p1; // shared_ptr,可以指向string
shared_ptr<list<int>> p2; // shared_ptr,可以指向int的list
默认初始化的智能指针中保存着一个空指针(参加2.3.2节,第48页),在1.3节中,我们将介绍初始化智能指针的其他方法。
智能指针的使用方式与普通指针类似。解引用一个智能指针以返回它指向的对象(的引用)。如果在一个条件判断中使用智能指针,效果就是检测它是否为空:
// 如果p1不为空,检查它是否指向一个空string
if (p1 && p1->empty())
*p1 = "hi"; // 如果p1指向一个空string,解引用p1,将一个新值赋予string
表12.1列出了 shared_ptr
和 unique_ptr
都支持的操作:
share_ptr 和 unique_ptr 都支持的操作 | 操作说明 |
---|---|
shared_ptr<T> sp | 空智能指针,可以指向类型为 T 的对象,里面保存着一个空指针 |
unique_ptr<T> up; | 同上 |
p | 将 p 用作一个条件判断,若 p 指向一个对象,则为 true |
*p | 解引用 p ,获取它指向的对象 |
p->mem | 等价于 (*p).mem |
p.get() | 返回 p 中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了 |
swap(p, q) | 交换 p 和 q 中的指针 |
p.swap(q) | 同上 |
只适用于 shared_ptr
的操作列在表12.2中:
share_ptr 独有的操作 | 操作说明 |
---|---|
make_shared<T>(args) | 返回一个 shared_ptr ,指向一个动态分配的类型为 T 的对象,并使用 args 初始化此对象 |
shared_ptr<T> p(q) | p 是 shared_ptr q 的拷贝。此操作会递增 q 中的计数器。q 中的指针必须能转换为 T* (参加4.11.2节,第143页) |
p = q | p 和 q 都是 shared_ptr ,所保存的指针必须能相互转换。此操作会递减 p 的引用计数,递增 q 的引用计数。若 p 的引用计数变为 0 ,则将其管理的原内存释放 |
p.unique() | 若 p.use_count() 为 1 ,返回 true ,否则返回 false |
p.use_count() | 返回与 p 共享对象的智能指针数量,可能很慢,主要用于调试 |
1.1.1 make_shared
函数
1.1.2 shared_ptr
的拷贝和赋值
1.1.3 shared_ptr
自动销毁所管理的对象
1.1.4 使用了动态生存期的资源的类
1.1.5 定义 StrBlob
类
1.1.6 StrBlob
构造函数
1.1.7 元素访问成员函数
1.1.8 StrBlob
的拷贝、赋值和销毁
⭐️12.1.1节练习
练习12.1:在此代码的结尾,b1
和 b2
各包含多少个元素?
StrBlob b1;
{
StrBlob b2 = {"a", "an", "the"};
b1 = b2;
b2.push_back("about");
}
练习12.2:编写你自己的 StrBlob
类,包含 const
版本的 front
和 back()
。
练习12.3:StrBlob
需要 const
版本的 push_back
和 pop_back
吗?如果需要,添加进去。否则,解释为什么不需要。
练习12.4:在我们的 check
函数中,没有检查 i
是否大于 0
。为什么可以忽略这个检查?
练习12.5:我们未编写接受一个 initializer_list explicit
(参加7.5.4节,第264页)的构造函数。讨论这个设计策略的优点和缺点。