[C++ Primer] : 第12章: 动态内存
动态内存与只能指针
静态内存用来保存局部static对象, 类static数据成员以及定义在任何函数之外的变量. 栈内存用来保存定义在函数内的非static对象. 分配在静态或栈内存中的对象由编译器自动创建和销毁. 栈中的对象, 仅在其定义的程序块运行时才存在; static对象在使用之前分配, 在程序结束时销毁.
自由空间或堆用来存储动态分配的对象——即在程序运行时分配的对象. 动态对象的生存期由程序来控制, 也就是说, 当动态对象不在使用时, 我们的代码必须显示地销毁它们.
动态内存与智能指针
C++中动态内存的管理是通过new和delete运算符来完成.
新标准库提供了3种智能指针来管理动态对象, 它们可以自动释放所指的对象. 它们都定义在memory头文件中.
智能指针 |
---|
shared_ptr |
unique_ptr |
weak_ptr |
shared_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独有的操作:
make_shared<T>(args) 是一个标准库函数, 返回一个shared_ptr, 指向一个动态分配的类型为T的对象. 使用args初始化此对象, args为空, 则执行值初始化.
shared_ptr<T> p(q) p是shared_ptr q的拷贝, 此操作会递增q中的计数器, q中的指针必须能转换成T*
p = q 此操作会递增q的引用计数, 递减p的引用计数, 若p的引用计数变为0, 则将其管理的原内存释放
p.unique() 若p.use_count()为1, 返回true, 否则返回false
p.use_count() 返回与p共享对象的智能指针数量, 可能很慢, 主要用于调试.
每一个shared_ptr都有一个关联的计数器, 通常称其为引用计数. 拷贝一个shared_ptr, 引用计数会递增, 给shared_ptr赋予一个新值或被销毁, 计数器递减.
make_shared函数
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数. make_shared用其参数来构造给定的对象.
shared_ptr<string> p1; // 可以指向string
shared_ptr<list<string>> p2; // 可以指向string的list
shared_ptr<int> p3 = make_shared<int>(42); // 指向一个值为42的int的shared_ptr
shared_ptr<string> p4 = make_shared<string>(3, '9'); // 指向值为"999"的string
shared_ptr<int> p5 = make_shared<int>(); // 指向一个值初始化的int, 值为0
auto p6 = make_shared<vector<string>>();
auto p7(p6); // p6, p7指向相同的对象.
class MyClass
{
public:
typedef std::shared_ptr<MyClass> sptr;
static sptr make()
{
return sptr(new MyClass());
}
static sptr make2(int x, int y)
{
return make_shared<MyClass>(x, y);
}
MyClass(int x1 = 0, int y1 = 0): x(x1), y(y1) { }
~MyClass(){ }
private:
int x;
int y;
};
int main()
{
MyClass::sptr sp_mc = MyClass::make(); // 返回智能指针管理的动态类对象
sp_mc = MyClass::make2(10, 24); // sp_mc原来指向的对象内存会释放, 然后sp_mc指向一个新对象.
shared_ptr<MyClass> sp_mc2 = make_shared<MyClass>(14, 20);
return 0;
}
如果将shared_ptr存放在一个容器中, 而后不再需要全部元素, 而只使用其中一部分, 要用erase删除不再需要的那些元素.
程序使用动态内存的三个原因:
- 程序不知道自己需要多少资源, 如容器类是出于该原因而使用动态内存的典型例子.
- 程序不知道所需对象的准确类型.
- 程序需要在多个对象间共享数据.
直接管理内存
new与delete, new[]和delete[]成对使用.
在自由空间分配的内存是无名的, 因此new无法为其分配的对象命名, 而是返回一个指向该对象的指针.
默认情况下, 动态分配的对象是默认初始化的, 这意味着内置类型或组合类型的对象的值将是未定义的, 而类类型对象将用默认构造函数进行初始化.
也可以使用直接初始化方式, 可以使用传统的构造方式(使用圆括号), 在新标准下, 也可以使用列表初始化(使用花括号), 还可以进行值初始化, 只需在类型名后面跟一对空括号即可. 对于定义了自己的构造函数的类类型来说, 值初始化和默认初始化是一样的, 都会调用默认构造函数来初始化.
int *pi = new int(1024); // 直接初始化, 用传统的构造方式(使用圆括号)
int *ps = new string(10, '9'); // 直接初始化, 用传统的构造方式
vector<int> *pv = new vector<int>{0,1,2,3} // 直接初始化, 用初始化列表的方式
string *ps1 = new string; // 默认初始化, 空string, 调用默认构造函数
string *ps2 = new string(); // 值初始化, 初始化为空string
int *pi1 = new int; // 默认初始化, *pi1的值未定义
int *pi2 = new int(); // 值初始化为0
如果我们提供了一个括号包围的初始化器, 就可以使用auto来从此初始化器来推断我们想要分配的对象的类型. 由于编译器需要要初始化器的类型来推断要分配的类型, 只有当括号中仅有单一初始化器时才可以使用auto.
auto p1 = new auto(obj); // p1指向一个与obj类型相同的对象, 该对象用obj来初始化
auto p2 = new auto{a, b, c}; // 错误, 括号中只能有单个初始化器.
动态分配的const对象
一个动态分配的const对象必须进行初始化, 由于分配的对象是const的, new返回的指针是一个指向const的指针.
默认情况下, new如果不能分配所要求的内存空间, 它会抛出一个类型为bad_alloc的异常, 而C中的malloc则会返回一个NULL指针.
定位new: 定位new表达式允许我们向new传递额外参数.
int *p1 = new int; // 如果分配失败, new抛出std::bad_alloc
int *p2 = new (nothrow) int; // 如果分配失败, new返回一个空指针
传递给new一个有标准库定义的名为nothrow的对象, 意思是告诉它不能抛出异常, 如果这种形式的new不能分配所需的内存, 就会返回一个空指针, 而不会抛出异常. bad_alloc和nothrow都定义在new头文件中.
释放动态内存
delete接受一个指针, 指向我们想要释放的对象. delete表达式执行两个动作: 销毁给定的指针所指向的对象; 释放对应的内存.
传递给delete的指针必须指向动态分配的内存, 或者是一个空指针. 释放一块并非new分配的内存, 或者将相同的指针释放多次, 其行为是未定义的. 释放一个空指针总是没有错误的, 它们通常什么也不做.
通常情况下, 编译器不能分辨一个指针指向的是静态还是动态分配的对象, 类似的, 编译器也不能分辨一个指针所指向的内存是否已经被释放. 传递给delete一个非new分配的内存指针, 尽管大多数编译器可以编译通过, 但它们是错误的, 其行为将是未定义的.
虽然一个const对象的值不能被改变, 但它本身是可以被销毁的.
有内置指针(而非智能指针)管理的动态内存在被显示释放之前一直都会存在.
使用new和delete管理动态内存存在3个常见的问题:
- 忘记delete内存.
- 使用已经释放掉的内存.
- 同一块内存释放两次.
delete之后记得重置指针值. 在delete之后, 指针就变成了人们所说的空悬指针, 将nullptr赋予指针, 这样就清楚地指针指针不再指向任何对象. 但即使是这样, 这也只是提供了有限的保护, 因为还有可能有多个指针指向相同的内存. delete之后重置指针的办法只对这个指针有效, 对其他仍指向这块以释放内存的指针是没有作用的.
shared_ptr和new结合使用
如果不初始化一个智能指针, 它就会被初始化为一个空指针. 可以用new返回的指针来初始化智能指针. 接受指针参数的智能指针构造函数是explicit的, 因此不能将一个内置指针隐式转换为一个智能指针, 必须使用直接初始化形式来初始化一个智能指针
shared_ptr<double> p1;
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的函数不能在其返回语句中隐式转换一个普通指针.
return shared_ptr<int>(new int(p)); //正确, 显示地用int *创建shared_ptr<int>
}
默认情况下, 一个用来初始化智能指针的普通指针必须指向动态内存, 因为智能指针默认使用delete来释放它所关联的对象. 也可以将智能指针绑定到一个指向其他类型的资源的指针上, 但是为了这样做, 必须提供自己的操作来代替delete.
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来释放对象.
p.reset(q);
p.reset(q, d);
不要混合使用普通指针和智能指针
shared_ptr可以协调对象的析构, 但这仅限于自身的拷贝(也是shared_ptr)之间, 这也是为什么推荐使用make_shared而不是new的原因.
当将一个shared_ptr绑定到一个普通指针时, 我们就将内存的管理责任交给了这个shared_ptr, 一旦这样做了, 就不应该再使用内置指针来访问shared_ptr所指向的内存了.
void process(shared_ptr<int> ptr)
{
//do something
} // ptr离开作用域, 被销毁
shared_ptr<int> p(new int(42)); // 引用计数为1
process(p); // 拷贝p会递增其引用计数, 在process中其引用计数为2
int i = *p; // 正确, 引用计数值为1
int *x(new int(1024)); // 危险: x是一个普通指针, 而非智能指针
//process(x); // 错误, 不能隐式转换
process(shared_ptr<int>(x)); // 创建一个临时的shared_ptr对象传递给process, process结束后该临时对象会被销毁, 此时其引用计数为0, x指向的内存会被释放.
int j = *x; // 未定义的行为, x是一个空悬指针.
也不要使用get初始化另一个智能指针或为智能指针赋值:
智能指针的get函数返回一个智能指针, 指向智能指针管理的对象, 此函数是为了这样一种情况而设定: 我们需要向不能使用智能指针的代码传递一个内置指针. 使用get返回指针的代码不能delete此指针.
shared_ptr<int> p(new int(42));
int *q = p.get(); //正确, 但是注意不要让它管理的指针被释放
{
// 新的程序块
shared_ptr<int>(q) // 未定义行为, 两个独立的shared_ptr指向相同的内存
} // 程序块结束, q被销毁, 它指向的内存被释放
int foo = *p; // 未定义: p指向的内存已经被释放了
只有在确定代码不会delete指针的情况下才能使用get, 特别是永远不要使用get初始化另一个智能指针或者为另一个智能指针赋值.
其他shared_ptr操作
reset经常和unique一起使用, 来控制多个shared_ptr共享的对象.
if(!p.unique) // 如果不是唯一用户就做一份拷贝
p.reset(new string(*p));
*p += newVal; // 此时自己是唯一用户了
智能指针和异常
确保在异常发生后资源能被正确的释放.
struct destination;
struct connection;
connection connect(destination*);
void disconnect(connection);
void f(destination &d/*其他参数*/)
{
// 获得一个连接; 记住使用完后要关闭它
connection c = connect(&d);
// 使用连接
// 如果在f退出前忘记调用disconnect或是由于异常而无法调用disconnect, 就无法关闭c了.
}
// 使用shared_ptr来保证connection被正确关闭, 这是一种有效的方法.
void end_connection(connection *p) // 提供自己的删除器
{
disconnect(*p);
}
void f(destination &d/*其他参数*/)
{
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection); // 默认会使用delete来释放资源, 但是可以提供自己的删除器
//使用连接
//当f退出时(即使是由于异常而退出), connection会被正确关闭
}
智能指针陷阱:
- 不要使用相同的内置指针初始化或reset多个智能指针. 因为这会导致个智能指针独立创建, 各自的引用计数器是独立的, 即不知道还有其他智能指针指向同一块内存.
- 不要delete有get()返回的指针.
- 不要使用get()初始化或reset另一个智能指针.
- 如果使用get()返回的指针, 谨记当最后一个对应的智能指针销毁后, 你的指针就变为无效了.
- 如果你使用智能指针管理的资源不是new分配的内存, 记住传递给它一个删除器.
unique_ptr
与shared_ptr不同, 任一时刻, 只能有一个unique_ptr指向一个给定的对象, 也没有类似make_shared的标准库函数返回一个unique_ptr. 当我们定义一个unique_ptr时, 需要将其绑定到一个new返回的指针上. 类似与shared_ptr, 初始化unique_ptr时也必须采用直接初始化方式. unique_ptr由于独占对象, 故不支持普通的拷贝和赋值操作.
unique_ptr<T> u1 空unique_ptr, u1会使用delete来释放其指针, u2使用一个类型为D的可调用对象来释放指针
unique_ptr<T, D> u2
unique_ptr<T, D> u(d) 空unique_ptr, 使用类型为D的可调用对象d代替delete.
u = nullptr 释放u指向的对象, 将u置空
u.release() u放弃对指针的控制权, 返回指针, 并将u置空
u.reset() 释放u所指的对象, 如果提供了内置指针q, 令u指向这个对象, 否则将u置空.
u.reset(q)
u.reset(nullptr)
虽然无法拷贝或赋值unique_ptr, 但可以通过调用release或reset将指针的所有权从一个unique_ptr转移给另一个unique.
unique_ptr<string> p2(p1.release()); // p1放弃对指针的控制权, 返回指针, 并将p1置空
unique_ptr<string> p3(new string("Trex"));
p2.reset(p3.release()); // 释放p2, p3置空, p2指向p3曾将指向的内存.
p2.release(); // 错误, p2不会释放内存, 而且丢失了指针.
auto p = p2.release(); // 正确, 但是必须记得delete(p)
release会切断unique_ptr与它原来管理的对象之间的联系. reset则会释放其所指的资源.
不能拷贝unique_ptr的规则有一个例外
我们可以拷贝或赋值一个将要被销毁的unique_ptr, 最常见的例子是从函数返回一个unique_ptr.
unique_ptr<int> clone(int p)
{
return unique_ptr<int>(new int(p));
}
unique_ptr<int> clone(int p)
{
unique_ptr<int> ret(new int (p));
// ...
return ret;
}
编译器知道要返回的对象将要被销毁, 在此情况下, 编译器执行一种特殊的拷贝.
向unique_ptr传递删除器
// 其他函数同shared_ptr
void f(destination &d/*其他参数*/)
{
connection c = connect(&d); // 打开连接
unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
//使用连接
//当f退出时(即使是由于异常而退出), connection会被正确关闭
}
weak_ptr
weak_ptr是一种不控制所指对象生存期的智能指针, 它指向一个由shared_ptr管理的对象, 将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数. 一旦最后一个指向对象的shared_ptr被销毁, 对象就会被释放, 即使有weak_ptr指向对象, 对象还是会被释放.
weak_ptr<T> w 空weak_ptr
weak_ptr<T> w(sp) 与shared_ptr指向相同对象的weak_ptr. T必须能够转换为sp指向的类型
w = p p可以是一个weak_ptr或一个shared_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.
当创建一个weak_ptr时, 需要用一个shared_ptr来初始化它.
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp弱共享p, p的引用计数未改变
由于对象可能不存在, 我们不能直接用weak_ptr直接访问对象, 而必须调用lock. 此函数检查weak_ptr指向的对象是否仍存在. 如果存在, lock返回一个指向共享对象的shared_ptr.
if(shared_ptr<int> np = wp.lock()) { // 先执行赋值, 再判断np是否为空
// 在if语句中, np与p共享对象
}
动态数组
大多数应用应该使用标准库容器而不是动态分配的数组. 使用容器更为简单, 更不容易出现内存管理错误并且可能有更好的性能.
new和数组
int *p = new int[get_size()]; // p指向第一个int的指针, 大小必须是整型, 但不必是常量
动态数组不是数组类型, 因此不能对动态数组调用begin和end.
默认情况下, new分配的对象都是默认初始化的, 可以对数组进行值初始化, 大小后面跟上一对空括号即可.
int *pia = new int[10]; // 10和未初始化的int
int *pia2 = new int[10](); // 10个值初始化的int
string *psa = new string[10]; // 10个空string
string *psa2 = new string[10](); // 10个空string
int *pia3 = new int[10]{1, 2, 3}; // 前3个用列表中对应的初始化器初始化, 剩余的进行值初始化
新标准中还可以提供一个元素初始化器的花括号列表. 如果初始化器数目小于元素数目, 剩余元素将进行值初始化, 如果初始化器数目大于元素数目, 则new表达式失败, 不会分配任何内存.
虽然可以用空括号对数组中的元素进行值初始化, 但不能在括号中给出初始化器, 这意味着不能用auto分配数组. new用于非数组时可以在括号内提供初始化器, 用于数组时则不行.
可以动态分配一个空数组, new会返回一个合法的非空指针, 它保证与new返回的其他任何指针都不相同, 此指针就像尾后指针一样, 不能解引用.
char arr[0]; // 错误, 不能定义长度为0的数组
char *cp = new char[0]; // 正确, 但是cp不能解引用
释放动态数组必须要使用delete[], 忽略方括号其行为是未定义的.
智能指针和动态数组:
标准库提供了一个可以管理new分配的数组的unique_ptr版本. 为了用一个unique_ptr管理动态数组, 必须在对象类型后面跟上一对空方括号.
unique_ptr<int[]> up(new int[10]{1, 2, 3});
for(size_t i = 0; i != 10; ++i)
up[i] = i; // 可以用下标访问数组中的每一个元素
up.reset(); // 调用delete[]释放动态内存
指向数组的unique_ptr不支持成员访问运算符(点和箭头运算符), 因为unique_ptr指向的是一个数组而不是单个对象. 可以使用下标运算符来访问数组中的元素.
指向数组的unique_ptr的操作:
unique_ptr<T[]> u;
unique_ptr<T[]> u(p);
u[i]; 返回u拥有的数组中位置i处的对象.
shared_ptr不直接支持管理动态数组, 如果希望用shared_ptr管理一个动态数组, 必须提供自己定义的删除器.
// 传递一个lambda作为删除器, 它使用delete[]来释放数组.
shared_ptr<int> sp(new int[10], [](int *p){ delete[] p; });
// shared_ptr没有定义[]运算符, 并且不支持指针的算数运算
for(size_t i = 0; i != 10; ++i)
*(sp.get() + i) = i; // 使用get()获取一个内置指针
sp.reset(); // 使用我们提供的lambda释放数组, 它使用delete[]
如果未提供删除器, 则代码是未定义的, 因为shared_ptr默认使用delete而不是delete[]来销毁对象.
shared_ptr未定义下标运算符, 而且智能指针类型不支持指针算术运算. 为访问数组中的元素, 必须用get()获取一个内置指针, 然后用它来访问数组元素.
allocator类
new将内存分配和对象构造结合在了一起. delete将对象析构和内存释放组合在一起. 这些使得new有一些灵活上的局限性.
当分配一大块内存时, 通常计划在这块内存上按需构造对象, 此时我们希望将内存分配和对象构造分离. 着意味着只在真正需要时才真正执行对象创建操作.
一般情况下, 将内存分配和对象构造组合在一起可能导致不必要的浪费.
string *const p = new string[n]; // 构造n个空的string
string s;
string *q = p;
while(cin >> s && q != p + n)
*q++ = s;
const size_t size = q - p;
// 使用数组
delete[] p;
new初始化并分配了n个string. 但是可能不需要n个string. 这样可能就创建了一些永远用不到的对象. 对于确实需要使用的对象, 初始化之后立即赋予新值, 每个使用到的元素都被赋值两次. 更重要的是, 那些没有默认构造函数的类就不能动态分配数组了.
allocator类定义在头文件memory中, 它提供一种类型感知的内存分配方法, 它分配的内存时原始的, 未构造的.
allocator是一个模板, 当一个allocator对象分配内存时, 它会根据给定的对象类型来确定恰当的内存大小和对齐位置.
标准库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之前, 用户必须对内存中的每个对象调用destory()
a.construct(p, args) p是一个T*的指针, 指向一块原始内存, args被传递给类型为T的构造函数, 在p所指的内存中构造一个对象.
a.destroy(p) p为T*类型的指针, 此算法对p所指的对象执行析构函数.
allocator<string> alloc; // 可以分配string的allocator对象
auto const p = alloc.allocate(n); // 分配n个未初始化的string
auto q = p; // q在后续操作中将指向最后构造的元素之后的位置.
alloc.construct(q++); // 空串
alloc.construct(q++, 3, 'c'); // ccc
alloc.construct(q++, "hi"); // hi
cout << *p << endl; // 正确, p指向第一个已经构造过的对象
cout << *q << endl; // 错误, q指向未构造的内存
while(q != p)
alloc.destory(--q); // 释放真正构造的string, 没有释放内存
alloc.deallocate(p, n); // 释放内存
allocator将内存分配释放和对象构造析构分成了两个阶段: 内存配置由allocate负责, 内存释放由deallocate负责. 对象构造操作由construct负责, 对象析构由destroy负责.
拷贝和填充未初始化内存的算法
这些函数在给定目的位置创建元素, 而不是由系统分配内存给它们.
uninitialized_copy(b, e, b2) 从迭代器b和e指出的输入范围中拷贝元素到迭代器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必须指向足够大的未构造的原始内存, 能够容纳给定数量的对象.
以上函数均返回一个指向最后一个构造的元素之后的位置的迭代器.