第十二章 动态内存
- 对象的生命周期:
- 全局对象在程序启动时分配,结束时销毁。
- 局部对象在进入程序块时创建,离开块时销毁。
- 局部
static
对象在第一次使用前分配,在程序结束时销毁。 - 动态分配对象:只能显式地被释放。
- 对象的内存位置:
- 静态内存用来保存局部
static
对象、类static
对象、定义在任何函数之外的变量。 - 栈内存用来保存定义在函数内的非
static
对象。 - 堆内存,又称自由空间,用来存储动态分配的对象。
- 静态内存用来保存局部
动态内存与智能指针
- 动态内存管理:
new
:在动态内存中为对象分配空间并返回一个指向该对象的指针。delete
:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
- 智能指针:
- 管理动态对象。
- 行为类似常规指针。
- 负责自动释放所指向的对象。
- 智能指针也是模板。
shared_ptr类
shared_ptr和unique_ptr都支持的操作:
操作 | 解释 |
---|---|
shared_ptr<T> sp unique_ptr<T> up | 空智能指针,可以指向类型是T 的对象 |
p | 将p 用作一个条件判断,若p 指向一个对象,则为true |
*p | 解引用p ,获得它指向的对象。 |
p->mem | 等价于(*p).mem |
p.get() | 返回p 中保存的指针,要小心使用,若智能指针释放了对象,返回的指针所指向的对象也就消失了。 |
swap(p, q) p.swap(q) | 交换p 和q 中的指针 |
shared_ptr独有的操作:
操作 | 解释 |
---|---|
make_shared<T>(args) | 返回一个shared_ptr ,指向一个动态分配的类型为T 的对象。使用args 初始化此对象。 |
shared_ptr<T>p(q) | p 是shared_ptr q 的拷贝;此操作会递增q 中的计数器。q 中的指针必须能转换为T* |
p = q | p 和q 都是shared_ptr ,所保存的指针必须能互相转换。此操作会递减p 的引用计数,递增q 的引用计数;若p 的引用计数变为0,则将其管理的原内存释放。 |
p.unique() | 若p.use_count() 是1,返回true ;否则返回false |
p.use_count() | 返回与p 共享对象的智能指针数量;可能很慢,主要用于调试。 |
智能指针问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Str{
public:
using size_type = vector<string>::size_type;
Str() ;
Str(initializer_list<string> li);
public:
shared_ptr<vector<string>> data;
void check(size_type i,const string &msg) const;
};
Str::Str(initializer_list<string> li) :data(make_shared<vector<string>>(li)){};
Str::Str() :data(make_shared<vector<string>>()){}; // 初始化一个空的 shared_ptr<> 指针
void test04(){
Str str1{"hello","jay"};
cout << str1.data << endl; // 0x600002cd8228
Str str2 = str1;
shared_ptr<vector<string>> str3;
// str3 = str2.data;
cout << str2.data << endl; // 0x600003850228
cout << str1.data.unique() << endl; // 0
cout << str1.data.use_count() << endl; //2
}
initializer_list 初始化问题
1
2
3
4
5
6
7
8
void method(initializer_list<string> li){
vector<string> vec(li.begin(),li.end());
for (const auto &item : vec)
cout << item << endl;
}
void test03(){
method({"hello","jay","mimi"});
}
- 使用动态内存的三种原因:
- 程序不知道自己需要使用多少对象(比如容器类)。
- 程序不知道所需要对象的准确类型。
- 程序需要在多个对象间共享数据。
直接管理内存
- 用
new
动态分配和初始化对象。new
无法为分配的对象命名(因为自由空间分配的内存是无名的),因此是返回一个指向该对象的指针。int *pi = new int(123);
- 一旦内存耗尽,会抛出类型是
bad_alloc
的异常。
- 用
delete
将动态内存归还给系统。- 接受一个指针,指向要释放的对象。
delete
后的指针称为空悬指针(dangling pointer)。
- 使用
new
和delete
管理动态内存存在三个常见问题:- 1.忘记
delete
内存。 - 2.使用已经释放掉的对象。
- 3.同一块内存释放两次。
- 1.忘记
- 坚持只使用智能指针可以避免上述所有问题。
shared_ptr和new结合使用
定义和改变shared_ptr的其他方法:
操作 | 解释 |
---|---|
shared_ptr<T> p(q) | p 管理内置指针q 所指向的对象;q 必须指向new 分配的内存,且能够转换为T* 类型 |
shared_ptr<T> p(u) | p 从unique_ptr u 那里接管了对象的所有权;将u 置为空 |
shared_ptr<T> p(q, d) | p 接管了内置指针q 所指向的对象的所有权。q 必须能转换为T* 类型。p 将使用可调用对象d 来代替delete 。 |
shared_ptr<T> p(p2, d) | p 是shared_ptr p2 的拷贝,唯一的区别是p 将可调用对象d 来代替delete 。 |
p.reset() | 若p 是唯一指向其对象的shared_ptr ,reset 会释放此对象。若传递了可选的参数内置指针q ,会令p 指向q ,否则会将p 置空。若还传递了参数d ,则会调用d 而不是delete 来释放q 。 |
p.reset(q) | 同上 |
p.reset(q, d) | 同上 |
智能指针与new相结合使用
1
2
3
4
5
6
7
8
9
10
11
void test05(){
shared_ptr<int> p1(new int(1024));
int *p2 = new int(10);
shared_ptr<int> p3(p2);
cout << *p3 << endl;
// 管理指针
User *u = new User(29);
shared_ptr<User> user(u);
cout << user->age << endl;
}
不要混用普通指针与智能指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void method1(shared_ptr<string> ptr){
cout << *ptr << endl;
cout << ptr.use_count() << endl; // 2
return;
}
void test07(){
shared_ptr<string> p(new string("hello jay"));
cout << p.use_count() << endl; // 1
method1(p);
cout << p.use_count() << endl; // 1
// 错误示范
string *str = new string("hello");
// method1(str); // 错误
method1(shared_ptr<string>(str)); // 正确的 但是内存会被释放掉
cout << *str << endl; // 乱码 内存被释放
}
将指针交给智能指针管理后,就不应该再使用内置指针来访问。
不要使用get初始化另外一个指针指针,
1
2
3
4
5
6
7
8
9
shared_ptr<int> p(new int(42));
int *q = p.get();
{
// 新程序块
shared_ptr<int> (q);
// 程序块结束 q被销毁、
}
int foo = *p; // 未定义
使用make_shared
1
2
3
4
5
void test10(){
shared_ptr<string> p = make_shared<string>("string");
cout << p << endl;
cout << *p << endl;
}
1
2
3
4
5
6
void test11(){
string *str = new string("hello jay");
shared_ptr<string> p(str);
cout << str << endl;
cout << p << endl;
}
reset操作,
1
2
3
4
if(!p.unique())
p.reset(new string(*p)); // 我们不是唯一用户,重新分配拷贝
*p += newVal; // 现在我们是唯一用户了 可以改变对象的值
智能指针和异常
如果使用智能指针,即使程序块由于异常过早结束,智能指针类也能确保在内存不需要的时候将其释放。
1 2 3 4 5 6 7 8 9 10 11 12
void f(){ shared_ptr<int> sp(new int(42)); // 发生异常 而且未被捕获 } // 函数结束时, shared_ptr 自动释放内存 void f(){ int *ip = new int(42); // 此时发生异常 且未被捕获 delete p; // 内存不会被释放,异常时 }
智能指针陷阱:
- 不用相同的内置指针初始化(或
reset
)多个智能指针 - 不
delete get()
返回的指针。 - 如果你使用
get()
返回的指针,记得当最后一个对应的智能指针销毁后,你的指针就无效了。 - 如果你使用智能指针管理的资源不是
new
分配的内存,记住传递给它一个删除器。
- 不用相同的内置指针初始化(或
unique_ptr
- 某一个时刻只能有一个
unique_ptr
指向一个给定的对象。 - 不支持拷贝或者赋值操作。
- 向后兼容:
auto_ptr
:老版本,具有unique_ptr
的部分特性。特别是,不能在容器中保存auto_ptr
,也不能从函数返回auto_ptr
。 - 虽然不支持拷贝与赋值操作,但是我们可以拷贝或者赋值一个将要被销毁的
unique_ptr
1 2 3 4
unique_ptr<int> clone(int p){ return unique_ptr<int> (new int(p)); }
unique_ptr操作:
操作 | 解释 |
---|---|
unique_ptr<T> u1 | 空unique_ptr ,可以指向类型是T 的对象。u1 会使用delete 来是释放它的指针。 |
unique_ptr<T, D> u2 | u2 会使用一个类型为D 的可调用对象来释放它的指针。 |
unique_ptr<T, D> u(d) | 空unique_ptr ,指向类型为T 的对象,用类型为D 的对象d 代替delete |
u = nullptr | 释放u 指向的对象,将u 置为空。 |
u.release() | u 放弃对指针的控制权,返回指针,并将u 置空。 |
u.reset() | 释放u 指向的对象 |
u.reset(q) | 令u 指向q 指向的对象 |
u.reset(nullptr) | 将u 置空 |
1
2
3
4
5
6
7
8
void test12(){
unique_ptr<string> p(new string("hello jay"));
cout << *p << endl;
unique_ptr<string> p2 ;
// p2 = p; // 错误 unique 为唯一指针
p2.reset(p.release());
cout << *p2 << endl;
}
1
2
3
4
5
6
7
// 删除问题
void test13(){
unique_ptr<string> p(new string("hello jay"));
p.release(); // 错误 p不会被释放内存
auto p2 = p.release(); // p的内存被释放 转为p2
delete(p2); // 释放p2
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 拷贝问题 原则上unique_ptr 是不允许拷贝赋值的
//1.
unique_ptr<string> method3(string str){
return unique_ptr<string> (new string(str));
}
//2. 还可以返回一个局部对象的拷贝
unique_ptr<string> method4(string str){
unique_ptr<string> p(new string(str));
return p;
}
void test14(){
auto p = method3("hello");
auto p2 = method4("hello");
cout << *p << endl;
cout << *p2 << endl;
}
传递销毁器与shared_ptr类似,但又不相同
weak_ptr
weak_ptr
是一种不控制所指向对象生存期的智能指针。- 指向一个由
shared_ptr
管理的对象,不改变shared_ptr
的引用计数。 - 一旦最后一个指向对象的
shared_ptr
被销毁,对象就会被释放,不管有没有weak_ptr
指向该对象。
weak_ptr操作:
操作 | 解释 |
---|---|
weak_ptr<T> w | 空weak_ptr 可以指向类型为T 的对象 |
weak_ptr<T> w(sp) | 与shared_ptr 指向相同对象的weak_ptr 。T 必须能转换为sp 指向的类型。 |
w = p | p 可以是shared_ptr 或一个weak_ptr 。赋值后w 和p 共享对象。 |
w.reset() | 将w 置为空。 |
w.use_count() | 与w 共享对象的shared_ptr 的数量。 |
w.expired() | 若w.use_count() 为0,返回true ,否则返回false |
w.lock() | 如果expired 为true ,则返回一个空shared_ptr ;否则返回一个指向w 的对象的shared_ptr 。 |
1
2
3
4
5
6
7
void test15(){
shared_ptr<string> p(new string("hello jay"));
weak_ptr<string> p1(p);
cout << p1.use_count() << endl;
auto p3 = p1.lock();
cout << p3.use_count() << endl;
}
1
2
3
4
5
6
7
8
9
void test15(){
shared_ptr<string> p(new string("hello jay"));
weak_ptr<string> p1(p);
// cout << *p1 << endl; // 因为p1 可能不存在 所以不可以直接访问p1
if(auto p2 = p1.lock()){ // 如果p1不存在,直接返回false 不会执行下列语句
cout << *p2 << endl;
cout << p2.use_count() << endl;
}
}
动态数组
new和数组
new
一个动态数组:- 类型名之后加一对方括号,指明分配的对象数目(必须是整型,不必是常量)。
- 返回指向第一个对象的指针。
int *p = new int[size];
delete
一个动态数组:delete [] p;
unique_ptr
和数组:- 指向数组的
unique_ptr
不支持成员访问运算符(点和箭头)。
- 指向数组的
操作 | 解释 |
---|---|
unique_ptr<T[]> u | u 可以指向一个动态分配的数组,整数元素类型为T |
unique_ptr<T[]> u(p) | u 指向内置指针p 所指向的动态分配的数组。p 必须能转换为类型T* 。 |
u[i] | 返回u 拥有的数组中位置i 处的对象。u 必须指向一个数组。 |
allocator类
- 标准库
allocator
类定义在头文件memory
中,帮助我们将内存分配和对象构造分离开。 - 分配的是原始的、未构造的内存。
allocator
是一个模板。allocator<string> alloc;
标准库allocator类及其算法:
操作 | 解释 |
---|---|
allocator<T> a | 定义了一个名为a 的allocator 对象,它可以为类型为T 的对象分配内存 |
a.allocate(n) | 分配一段原始的、未构造的内存,保存n 个类型为T 的对象。 |
a.deallocate(p, n) | 释放从T* 指针p 中地址开始的内存,这块内存保存了n 个类型为T 的对象;p 必须是一个先前由allocate 返回的指针。且n 必须是p 创建时所要求的大小。在调用deallocate 之前,用户必须对每个在这块内存中创建的对象调用destroy 。 |
a.construct(p, args) | p 必须是一个类型是T* 的指针,指向一块原始内存;args 被传递给类型为T 的构造函数,用来在p 指向的内存中构造一个对象。 |
a.destroy(p) | p 为T* 类型的指针,此算法对p 指向的对象执行析构函数。 |
allocator伴随算法:
操作 | 解释 |
---|---|
uninitialized_copy(b, e, b2) | 从迭代器b 和e 给定的输入范围中拷贝元素到迭代器b2 指定的未构造的原始内存中。b2 指向的内存必须足够大,能够容纳输入序列中元素的拷贝。 |
uninitialized_copy_n(b, n, b2) | 从迭代器b 指向的元素开始,拷贝n 个元素到b2 开始的内存中。 |
uninitialized_fill(b, e, t) | 在迭代器b 和e 执行的原始内存范围中创建对象,对象的值均为t 的拷贝。 |
uninitialized_fill_n(b, n, t) | 从迭代器b 指向的内存地址开始创建n 个对象。b 必须指向足够大的未构造的原始内存,能够容纳给定数量的对象。 |
- 定义在头文件
memory
中。 - 在给定目的位置创建元素,而不是由系统分配内存给他们。