- 动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时才会被销毁。
动态内存和智能指针
- 动态内存管理通过一对运算符完成,new:在动态内存中为对象分配空间并返回一个指向该对象的指针。delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
- 忘记释放内存会导致内存泄露;在尚有指针引用内存的情况下就释放了内存,会产生引用非法内存的指针。
- 为了更安全地使用指针,可以使用智能指针,智能指针能负责自动释放对象。shared_ptr允许多个指针指向同一个对象;unique_ptr独占所指向的对象;还有伴随类weak_ptr,是一种弱引用,指向shared_ptr所管理的对象。
- 智能指针的使用方法和普通指针类似,只是多了一些操作
p.get()
swap(p, q)
p.swap(q)
p.use_count()
p.unique()
shared_ptr类
- make_shared(init)是最安全的分配和使用动态内存的方法,make_shared函数在动态内存中分配一个对象,并用init初始化它,最后返回指向这个对象的shared_ptr。
shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<string> p4 = make_shared<string>(10, '9');
shared_ptr<int> p5 = make_shared<int>();
auto p6 = make_shared<vector<string>>();
- 每个shared_ptr都有个关联的计数器,称为引用计数。进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象
- 当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它的计数器就会递增。给shared_ptr赋予一个新值或shared_ptr被销毁时,计数器会递减,递减到0就会自动释放所管理的对象。
auto r = make_shared<int>(42);
r = q;
- 在最后一个shared_ptr销毁前内存都不会释放,所以忘记销毁不再需要的shared_ptr会导致浪费内存。比如将shared_ptr存入容器后操作,最后忘了erase,就会浪费内存。
- 程序使用动态内存有3种原因:1.程序不知道自己需要使用多少个对象 2.程序不知道所需对象的准确类型 3.程序需要在多个对象间共享数据。容器类就是出于第一个原因来使用动态内存的,而常见原因是允许多个对象共享相同的状态
直接管理内存
- 自由空间分配的内存是无名的,所以new无法为其分配的对象命名,而是返回指向该对象的指针:
int *pi = new int;
string *ps = new string;
int *pi = new int(1024);
int *pi1 = new int;
int *pi2 = new int();
vector<int> *pv = new vector<int>{1, 2, 3};
auto p1 = new auto(obj);
auto p2 = new auto{a, b, c};
动态分配的const对象
- 一个动态分配的const对象必须进行初始化。如果是一个定义了默认构造函数的类类型,其const对象可以隐式初始化
const int *pci = new const int(1024);
const string *pcs = new const string;
- 由于分配的对象是const的,new返回的指针式一个指向const 的指针
内存耗尽
- 如果内存用光,new不能分配要求的内存空间,它会抛出类型为bad_alloc的异常
- 写成下面这种形式能阻止new抛出异常,并且如果不能分配所需内存,它会返回一个空指针
int *p = new (nothrow) int;
释放动态内存
- 传递给delete的指针必须指向动态分配的内存,或是空指针
int i, *pi1 = &i;
int *pi2 = nullptr;
int *pi3 = new int(41), *pi4 = pi3;
delete i;
delete pi1;
delete pi2;
delete pi3;
delete pi4;
- 指向const动态对象的指针也能被delete
- 返回动态内存的指针的函数要注意记得释放内存
Foo* factory(T arg)
{
return new Foo(arg);
}
void use_factory(T arg)
{
Foo *p = factory(arg);
}
- 由内置指针管理的动态内存在被显式释放前将一直都存在
- delete一个指针后指针已经无效,但是会变成“空悬指针”。就是指向一块曾经保存数据对象但现在已经无效的内存的指针。避免空悬指针的方法是在指针即将离开其作用域之前再释放掉它关联的内存
- 如果需要保留指针,可以在delete后将nullptr赋予指针,表明指针不指向任何对象
int *p(new int(42));
delete p;
p = nullptr;
shared_ptr和new结合使用
- 接受指针参数的智能指针构造函数是explicit的,所以不能讲一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针
shared_ptr<int> p1(new int(1024));
shared_ptr<T> p(q)
shared_ptr<int> p2 = new int(1024);
shared_ptr<int> clone(int p) { return new int(p); }
不要混合使用普通指针和智能指针
- shared_ptr可以协调对象的析构,但是仅限于自身的拷贝。所以最好使用make_shared而不是new,这样就能在分配对象的同时将shared_ptr与之绑定
- 正确操作传递shared_ptr的函数
void process(shared_ptr<int> ptr)
{
}
- 上面的函数传参过程中会将实参拷贝到ptr中,因为拷贝所以会递增其引用计数,也就是说在precess运行过程中引用计数至少为2,这样当process结束时局部变量ptr被销毁,引用计数减1,ptr指向的内存不会被释放。使用这个函数的正确方法是传递给它一个shared_ptr。
shared_ptr<int> p(new int(42));
process(p);
int *i = p;
- 不能给process传递一个内置指针,但是能传递一个临时的shared_ptr,这样就很可能导致错误
int *x(new int(1024));
process(shared_ptr<int>(x));
int *i = x;
不要使用get初始化另一个智能指针,或为智能指针赋值
- 智能指针类型有个get函数,返回一个指向智能指针管理对象的内置指针,目的是向不需要使用智能指针的代码传递一个内置指针。但是get返回的指针的代码不能delete此指针!
shared_ptr<int> p(new int(1024));
int *q = p.get();
{
shared_ptr<int>(q);
}
int foo = *p
- 只有在确定代码不会delete指针的情况下才能使用get。并且永远不要用get初始化另一个智能指针或者为另一个智能指针赋值
其他shared_ptr操作
- reset可以将一个新的指针赋予一个shared_ptr,并且更新计数。
p.reset(new int(1024));
- reset一般配合unique使用
if (!p.unique())
p.reset(new string(*p));
*p += newVal;
- 使用智能指针,即使程序块因为某些原因提前结束,智能指针类也能确保在内存不再需要时释放
void f()
{
shared_ptr<int> sp(new int(42));
}
void f()
{
int *ip = new int(42);
delete ip;
}
shared_ptr注意事项
- 不使用相同的内置指针初始化(或reset)多个智能指针
- 不delete get()返回的指针
- 不使用get()初始化或reset另一个智能指针
- 如果使用get()返回的指针,记住当最后一个对应的智能指针销毁后,get()返回的指针就变为无效了
unique_ptr
- unique_ptr没有类似make_shared的函数,如果要定义一个unique_ptr,需要将其绑定到一个new返回的指针上。初始化unique_ptr也必须使用直接初始化
unique_ptr<int> p2(new int(42));
- 因为unique_ptr只能有一个对象,所以unique_ptr不支持普通的拷贝和赋值操作
unique_ptr<string> p1(new string("***"));
unique_ptr<string> p2(p1);
unique_ptr<string> p3;
p3 = p1;
- 虽然不能拷贝或赋值,但是可以通过release或reset将指针的所有权从一个(非const的)unique_ptr转移给另一个unique_ptr
u.release()
u.reset()
u.reset(q)
unique_ptr<string> p1(new string("***"));
unique_ptr<string> p2(p1.release());
unique_ptr<string> p3(new string("###"));
p2.reset(p3.release());
- 调用release会切断unique_ptr和原来管理的对象间的联系。release返回的指针通常用来初始化另一个智能指针或给另一个智能指针赋值。
p2.release();
auto p = p2.release();
weak_ptr
- weak_ptr是不控制所指向对象生存期的智能指针,它指向一个shared_ptr管理的对象
- 将weak_ptr绑定到shared_ptr不会改变shared_ptr的引用计数。且最后一个指向对象的shared_ptr被销毁,即使weak_ptr指向着对象,对象还是会被释放。所以它是一种弱共享
- 创建一个weak_ptr要用shared_ptr来初始化它
auto p = make_shared<int>(42);
weak_ptr<int> wp(p);
- 因为对象可能不存在,所以不能用weak_ptr直接访问对象,而要用lock。lock函数检查weak_ptr指向的对象是否存在,若存在就返回指向共享对象的shared_ptr
if (shared_ptr<int> np = wp.lock())
- weak_ptr的作用就是阻止用户访问一个不再存在的对象
智能指针和哑类
- 不是所有类都能良好的定义析构函数,如果没有定义析构函数释放资源,可能会遇到和动态内存相同的错误。可以使用类似于管理动态内存的方法来管理没有定义良好析构函数的类
struct destination;
struct connection;
connection connect(destination*);
void disconnect(connection);
void f(destination &d)
{
connection c = connect(&d);
}
- 可以使用shared_ptr来保证connection被正确关闭
void end_connection(connection *p) { disconnect(*p); }
```cpp
void f(destination &d)
{
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
}
- unique_ptr管理删除器的方式和shared_ptr不同
unique<objT, delT> p (new objT, fcn);
void f(destination &d)
{
connection c = connect(&d);
unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
}
动态数组
- new分配要求数量的对象,并返回指向第一个对象的指针
int *pia = new int[10];
- 动态数组实际上并不是一个数组。就像上面,用new分配一个数组时,其实得到的是一个int指针(个人理解其实就是给了10个int的动态内存,然后动态内存没有“数组”的概念)
- 因为不是数组,所以不支持数组的一些用法,比如范围for语句、用begin和end等
- 在大小后面跟一对括号可以实现值初始化
int *pia = new int[10];
int *pia2 = new int[10]();
- 动态分配空数组是合法的。对长度为0的数组来说,此指针就像是尾后指针,它不能解引用
char arr[0];
char *cp = new char[0];
- 使用delete []释放内存,数组中的元素按逆序销毁
delete [] pa;
- 能配合使用智能指针unique_ptr和动态数组,但是当一个unique_ptr指向数组时,不能使用点和箭头成员运算符(因为指向的是整个数组而不是单个对象)。但是可以使用下标运算符
unique_ptr<int[]> up(new int[10]);
for (size_t i = 0; i != 10; i++)
up[i] = i;
up.release();
allocator类
- 我们分配单个对象时,通常希望将内存分配和对象初始化组合在一起(我们知道对象应有什么值),但分配一大块内存时,我们往往计划在这块内存上按需构造对象→将内存分配和对象构造分离,即我们可以分配大块内存,但只在需要时才真正执行对象创建操作。allocator实现了这个操作
allocator<string> alloc;
auto const p = alloc.allocate(n);
auto q = p;
alloc.construct(q++);
alloc.construct(q++, 10, 'c');
alloc.construct(q++, "hi");
- 为了使用allocate返回的内存,必须用construct构造对象。使用未构造的内存,行为是未定义的
- 用完对象后必须对每个构造的元素使用destroy来销毁(只能对构造了的元素进行destroy)
while (q != p)
alloc.destroy(--q);
- 销毁后可以用来保存其他string,也可以归还系统
alloc.deallocate(p, n);
函数 | 作用 |
---|
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必须指向足够大的未构造的原始内存,能够容纳给定数量的对象 |
auto p = alloc.allocate(vi.size(), 2);
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
uninitialized_fill_n(q, vi.size(), 42);