《C++ Primer》第12章 动态内存(一)
参考资料:
- 《C++ Primer》第5版
- 《C++ Primer 习题集》第5版
我们的程序目前只用过静态内存和栈内存。静态内存用来保存局部 static
对象、类 static
成员、定义在任何函数之外的变量;栈内存用来保存定义在函数内的非 static
对象。分配在静态内存和栈内存的对象由编译器自动创建和销毁,
除了静态内存和栈内存,每个程序还拥有一个内存池,被称作自由空间(free store)或堆(heap)。程序用堆来存储动态分配(dynamic allocate)的对象。动态对象的生存周期由程序控制,当动态对象不再使用时,代码必须显式地销毁它们。
12.1 动态内存与智能指针(P400)
在 C++ 中,动态内存的管理是通过一对运算符完成的:new
在动态内存中为对象分配空间并返回一个指向该对象的指针;delete
接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
动态内存的使用很容易出现问题:有时我们会忘记释放内存;有时我们会在尚有指针引用内存的情况下就释放内存。
为了更容易和更安全地使用动态内存,新标准库提供了两种智能指针(smart pointer)类型来管理动态对象。与普通指针的主要不同点在于,智能指针可以自动释放对象。shared_ptr
允许多个指针指向同一个对象;unique_str
“独占”所指对象。此外,标准库还定义了一个名为 weak_ptr
的伴随类,它是一种弱引用,指向 shared_ptr
所管理的对象。这三种类型都定义在头文件 memory
中。
12.1.1 shared_ptr
类(P400)
智能指针是模板,我们在创建智能指针时必须提供指针指向的类型:
shared_ptr<stirng> p1; // 空指针
shared_ptr<list<int>> p2;
智能指针的使用方式和普通指针类似:解引用一个智能指针返回它所指的对象;在条件判断中使用智能指针,就是检测它是否为空:
if(p1 && p1->empty()){
*p1 = "hi";
}
make_shared
函数
分配和使用动态内存最安全的做法是调用一个定义在头文件 memory
中,名为 make_shared
的标准库函数,该函数在动态内存中分配并初始化一个对象,返回指向此对象的 shared_ptr
。
使用 make_shared
时,必须指定要创建的对象的类型:
// p3指向值为7的int对象
shared_ptr<int> p3 = make_shared<int>(7);
// p4指向值为“999”的string对象
shared_ptr<string> p4 = make_shared<string>(3, '9');
// p5指向值为0(值初始化)的int对象
shared_ptr<int> p5 = make_shared<int>();
类似顺序容器的 emplace
成员,make_shared
用参数来构造对象,如果我们不传递任何参数,对象就会进行值初始化。
shared_ptr
的拷贝和赋值
每个 shared_ptr
对象都会记录有多少个 shared_ptr
指向相同的对象:
auto p = make_shared<int>(7);
auto q(p);
cout << p.use_count() << ' ' << q.use_count(); // 输出2 2
我们可以认为每个 shared_ptr
都有一个关联的计数器,称为引用计数(reference count)。当我们拷贝一个 shared_ptr
时(如拷贝构造、参数传递、作为函数返回值),它所关联的计数器会递增;当一个 shared_ptr
被赋予新值或被销毁时,它所关联的计数器会递减。
一旦一个 shared_ptr
的计数器变为 0 ,它就会自动释放自己管理的对象。
auto r = make_shared<int>(42);
r = q; // 递增q所指对象的引用计数
// 递减r原来所指对象的引用计数
// r原来所指对象的计数器变为0,自动释放
shared_ptr
自动销毁所管理的对象
shared_ptr
通过析构函数自动销毁对象。
shared_ptr
还会自动释放相关联的内存
使用了动态生存期的资源的类
程序使用动态内存出于以下三种原因:
- 程序不知道自己需要使用多少对象
- 程序不知道对象的准确类型
- 程序需要在多个对象间共享数据
目前为止,我们使用过的类中,分配的资源与对应对象的生存期一致。例如,每个 vector
“拥有”自己的元素,当我们拷贝一个 vector
时,原 vector
和副本 vector
中的元素是相互分离的:
vector<int> v1 = { 0,1,2 };
vector<int> v2;
v2 = v1;
v1.clear();
cout << v2.size(); // 输出为3
假定我们要定义一个 Blob
类,保存一组元素,希望 Blob
对象的不同拷贝之间共享元素。
定义StrBlob
类
由于还没有学习模板的相关知识,所以我们先定义一个管理 string
的类,命名为 StrBlob
:
class StrBlob {
public:
using size_type = vector<string>::size_type;
StrBlob();
StrBlob(initializer_list<string> il);
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
void push_back(const string &t) { data->push_back(t); }
void pop_back();
string &front();
string &back();
private:
// 使用shared_ptr实现数据共享
shared_ptr<vector<string>> data;
void check(size_type i, const string &msg) const;
};
StrBlob
构造函数
StrBlob::StrBlob() :data(make_shared<vector<string>>()) { }
StrBlob::StrBlob(initializer_list<string> il):
data(make_shared<vector<string>>(il)){ }
元素访问成员
void StrBlob::check(size_type i, const string &msg)const {
if (i >= data->size()) {
throw out_of_range(msg);
}
}
string &StrBlob::front() {
check(0, "front on empty StrBlob");
return data->front();
}
string &StrBlob::back() {
check(0, "back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back() {
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
StrBlob
的拷贝、赋值和销毁
StrBlob
使用默认版本的拷贝、赋值和析构函数。
12.1.2 直接管理内存(P407)
使用new
动态分配和初始化对象
在堆中分配的内存是无名的,因此 new
无法为其分配的对象命名,而是返回一个指向该对象的指针:
int *pi = new int;
默认情况下,动态分配的对象执行默认初始化。我们可以使用直接初始化来初始化一个动态分配的对象:
int *pi = new int(7);
int *ps = new string(3, '9');
vector<string> *pv = new vector<string>{"hi", "hello"};
也可以对动态分配的对象进行值初始化,只需在类型名后面跟一对空括号即可:
int *pi1 = new int; // 默认初始化
int *pi2 = new int(); // 值初始化
我们可以使用 auto
从初始化器推断我们要分配的对象的类型,但仅支持单一初始化器:
string str = "hello";
auto p1 = new auto(str); // p为string*
auto p2 = new auto{str, str}; // 错误
动态分配的const
对象
const int *pci = new const int(1024);
内存耗尽
如果 new
不能分配所要求的空间,它会抛出一个类型为 bad_alloc
的异常:
int *p1 = new int; // 分配失败则抛出bad_alloc异常
int *p2 = new (nothrow) int; // 分配失败则返回空指针
bad_alloc
和 nothrow
都定义在头文件 new
中。
释放动态内存
delete
销毁给定指针指向的对象,释放对应的内存:
delete p;
指针值和delete
我们传递给 delete
的指针必须指向动态分配的内存,或者是一个空指针。释放一块非 new
分配的内存,或多次释放相的指针值的行为是未定义的。
const
对象的值不能改变,但本身可以被销毁:
const int *pci = new const int(7);
delete pci;
动态对象的生存期直到被释放为止
对于一个由内置指针管理的动态对象,直到被显式释放前它都是存在的:
Foo* factory(T arg){
return new Foo(arg);
}
void use_factory(T arg){
Foo *p = factory(arg);
}
当 use_factory
返回时,p
被销毁,但其指向的动态内存却没有被释放。
12.1.3 shared_ptr
和new
结合使用(P412)
我们可以用 new
返回的指针来初始化智能指针:
shared_ptr<int> p(new int(7));
接受指针参数的智能指针构造函数是 explicit
的,因此我们必须使用直接初始化形式:
shared_ptr<int> p1 = new int(1024); // 错误,不能隐式转换
shared_ptr<int> p2(new int(1024));
shared_ptr<int> clone(int p){
return new int(p);
} // 错误
shared_ptr<int> clone(int p){
return shared_ptr<int>(new int(p));
} // 正确
默认情况下,智能指针使用 delete
释放它关联的对象。我们也可以提供自己的操作来替代 delete
:
似乎没有
shared_ptr<T> p(p2, d)
这个构造函数,书上是不是写错了🤔
不要混用普通指针和智能指针
shared_ptr
可以协调对象的析构,但这仅限于其自身的拷贝。考虑下面的函数:
void process(shared_ptr<int> ptr){
...
}
process
采用值传递,实参会拷贝到 ptr
中,导致引用计数递增。如果我们尝试混用普通指针和 shared_ptr
:
int *x = new int(1024);
process(x); // 错误,不能将int*隐式转换为shared_ptr<int>
process(shared_ptr<int>(x)); // 合法,但x指向的内存会被释放!
// 此时x已经变成空悬指针
当我们将一个 shared_ptr
绑定到一个普通指针后,就不应该再使用该普通指针了。
也不要使用get
初始化另一个智能指针或为智能指针赋值
智能指针定义了名为 get
的成员函数,返回一个内置指针,指向智能指针管理的对象。
虽然编译器不会给出报错信息,但将另一个智能指针绑定到 get
返回的指针是错误的:
shared_ptr<int> p1 = make_shared<int>(7); // 引用计数为1
int *q = p1.get();
{
// 两个独立的shared_ptr指向相同的内存,引用计数均为1
shared_ptr<int> p2(q);
} // p2被销毁,进而导致p1指向的内存被释放
int foo = *p1; // 未定义
不要 delete
通过 get
得到的指针,也不要用 get
得到的指针初始化另一个智能指针或者为另一个智能指针赋值。
其他shared_ptr
操作
我们可以用 reset
来将一个新的指针赋予一个 shared_ptr
:
p = new int(1024); // 错误
p.reset(new int(1024));
reset
常常与 unique
一起使用:
if(!p.unique())
p.reset(new string(*p)); // 如果p不是唯一用户,则分配新的拷贝
*p += newVal; // p为唯一用户,可以随意修改对象的值
练习
这道题涉及到了
explicit
构造函数、参数传递等问题,有些细节我还不是很清楚,目前只能给出一种相对合理的理解。假设有函数f(int a)
,然后我们调用它f(b)
,此时参数初始化的过程等价于执行int a = b
。所以上面题目中的 b) 实际上执行了shared_ptr<int> ptr = temp
,temp
为临时量,类型为int*
,而这条语句上执行的是拷贝初始化(尽管编译器可能优化为直接初始化),这不符合explicit
的要求。
12.1.4 智能指针和异常(P415)
使用智能指针可以确保在异常发生后资源能被正确释放:
void f(){ // 普通指针
int *ip = new int();
// 此时代码抛出一个异常,且在f中未被捕获
// ip被销毁,其指向的内存没有被释放
delete ip;
}
void f(){ // 智能指针
shared_ptr<int> sp(new int());
// 此时代码抛出一个异常,且在f中未被捕获
// sp被销毁的同时,其管理的内存也被释放
}
智能指针和哑类
有些类的析构函数并不负责释放资源,特别是为 C 和 C++ 两种语言设计的类,通常要求用户显式释放所使用的资源。
假设我们正在使用一个 C 和 C++ 都使用的网络库:
struct destination; // 表示我们正在连接什么
struct connection; //使用连接所需的信息
connection connect(destination *); // 打开连接
void disconnect(connection); // 关闭给定的连接
void f(destination &d /* 其他参数 */) {
connection c = connect(&d);
// 如果在f退出前忘记调用disconnect,就无法关闭c了
}
使用 shared_ptr
可以有效解决上述问题。
使用我们自己的释放操作
为了用 shared_ptr
来管理一个 connection
,我们必须定义一个删除器(deleter) 函数来代替 delete
:
void end_connection(connection *p) { disconnect(*p); }
void f(destination &d /* 其他参数 */) {
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
// 当p被销毁时,调用end_connection
}
为了正确使用智能指针,我们必须坚持一些基本规范:
- 不使用相同的内置指针初始化或
reset
多个智能指针。- 不
delete
get()
返回的指针。- 使用
get()
返回的指针时,记住最后一个对应的指针销毁后,指针就变为无效了。- 如果智能指针管理的资不是
new
分配的内存,记住传递一个删除器。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!