第十二章 动态内存
- 目前为止使用的对象都有着严格定义的生存期。使用内存池中静态区或栈区。
- 全局对象在程序启动时分配,在程序结束时销毁。
- 对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。
- 局部static对象在第一次使用前分配,在程序结束时销毁。
- 动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。使用内存池中自由空间或堆区。
12.1 动态内存与智能指针
- new,在动态内存中为对象分配空间并返回一个指向该对象的指针。
- delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
- 动态内存的使用很容易出问题:
- 新的标准库提供了两种智能指针:
- shared_ptr允许多个指针指向同一个对象。
- unique_ptr则“独占”所指向的对象。
- weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。
- 这三种类型都定义在memory头文件中。
12.1.1 shared_ptr类
shared_ptr<string> p1;
shared_ptr<list<int>> p2;
if (p1 && p1->empty())
{
*p1 = "hi";
}
shared_ptr和unique_ptr都支持的操作 | 解释 |
---|
shared_ptr sp | 空智能指针,可以指向类型为T的对象 |
unique_ptr up | std::string |
p | 将p用作一个条件判断,若p指向一个对象,则为true |
*p | FString |
p->mem | 等价于(*p).mem |
p.get() | 返回p中保存的指针,要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了。 |
swap(p, q) | 交换p和q中的指针。 |
p.swap(q) | 同上。 |
shared_ptr独有的操作 | 解释 |
---|
make_shared(args) | 返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化此对象。 |
shared_ptr 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共享对象的智能指针数量,可能很慢,主要用于调试。 |
- 最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。
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指向相同的对象:
auto p = make_shared<int>(42);
auto q(p);
- 每个shared_ptr都有一个关联的计数器,被称为引用计数。
- 一旦一个shared_ptr的计数器变为0,就会自动释放自己所管理的对象
r = q;
- shared_ptr的析构函数会递减它所指向的对象的引用计数,如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。
shared_ptr<Foo> factory(T arg)
{
return make_shared<Foo>(arg);
}
- 由于p是use_factory的局部变量,在use_factory结束时它将被销毁。
void use_factory(T arg)
{
shared_ptr<Foo> p = factory(arg);
}
- 如果有其他shared_ptr也指向这块内存,它就不会被释放掉。
void use_factory(T arg)
{
shared_ptr<Foo> p = factory(arg);
return p;
}
- 程序使用动态内存出于以下三种原因之一:
- 程序不知道自己需要使用多少对象。
- 程序不知道所需对象的准确类型。
- 程序需要在多个对象间共享数据。
- 当我们拷贝一个vector时,原vector和副本vector中的元素是相互分离的。
vector<string> v1;
{
vector<string> v2 = {"a", "an", "the"};
v1 = v2;
}
- 假定希望定义一个名为Blob的类,与容器不同,希望Blob对象的不同拷贝之间共享相同的元素。
- 两个对象共享底层的数据,当某个对象被销毁时,不能单方面地销毁底层数据。
Blob<string> b1;
{
Blob<string> b2 = {"a", "an", "the"};
b1 = b2;
}
- 使用动态内存的一个常见原因是允许多个对象共享相同的状态。
- 贴一个学习时老师徒手撸的简单智能指针类。
- Main.cpp
#include <iostream>
#include "AutoPoint.h"
#include "PointReferenceManager.h"
using namespace std;
class A
{
public:
int Age;
int Num;
int* pI;
A()
{
Num = 10;
cout << "普通构造函数" << endl;
pI = new int;
}
A(A& other)
{
cout << "拷贝构造函数" << endl;
}
A(A&& other)
{
cout << "移动构造函数" << endl;
pI = other.pI;
other.pI = nullptr;
}
~A()
{
cout << "析构函数" << endl;
if (pI)
{
delete pI;
}
}
};
class B
{
public:
A a;
int Num;
~B()
{
cout << "Free B" << endl;
}
};
template<typename T>
class Pointer
{
public:
Pointer():BindA(nullptr){}
~Pointer()
{
if (BindA)
{
delete BindA;
}
}
public:
T* BindA;
};
A Fun()
{
A a;
return a;
}
AutoPoint<A> GetA()
{
AutoPoint<A> pa(new A);
pa->Num = 100;
return pa;
}
int main()
{
AutoPoint<A> pa = GetA();
cout << pa->Num << endl;
system("Pause");
}
#pragma once
struct NULLPOINTER
{
};
template<typename T>
class AutoPoint
{
public:
~AutoPoint();
AutoPoint():BindPoint(nullptr){}
explicit AutoPoint(T* OutPoint);
AutoPoint(AutoPoint<T>& Other);
AutoPoint(AutoPoint<T>&& Other);
T* operator->();
AutoPoint<T>& operator=(NULLPOINTER* Point);
AutoPoint<T>& operator=(AutoPoint<T>& Other);
AutoPoint<T>& operator=(AutoPoint<T>&& Other);
bool operator==(const AutoPoint<T>& Other);
bool operator!=(const AutoPoint<T>& Other);
void* operator new(size_t) = delete;
explicit operator bool()
{
return BindPoint != nullptr;
}
void Free();
public:
T* BindPoint;
};
template<typename T>
AutoPoint<T>::AutoPoint(AutoPoint<T>&& Other)
{
if (Other.BindPoint)
{
BindPoint = Other.BindPoint;
Other.BindPoint = nullptr;
}
}
template<typename T>
AutoPoint<T>& AutoPoint<T>::operator=(AutoPoint<T>&& Other)
{
if (Other.BindPoint)
{
Free();
BindPoint = Other.BindPoint;
Other.BindPoint = nullptr;
}
}
template<typename T>
bool AutoPoint<T>::operator!=(const AutoPoint<T>& Other)
{
return BindPoint != Other.BindPoint;
}
template<typename T>
bool AutoPoint<T>::operator==(const AutoPoint<T>& Other)
{
return BindPoint == Other.BindPoint;
}
template<typename T>
AutoPoint<T>& AutoPoint<T>::operator=(AutoPoint<T>& Other)
{
if (!Other.BindPoint)
{
return *this;
}
Free();
BindPoint = Other.BindPoint;
PointReferenceManager::Get()->AddPointReference(BindPoint);
return *this;
}
template<typename T>
AutoPoint<T>& AutoPoint<T>::operator=(NULLPOINTER* Point)
{
if (!Point)
{
Free();
}
return *this;
}
template<typename T>
void AutoPoint<T>::Free()
{
if (BindPoint)
{
int Counter = PointReferenceManager::Get()->RemovePointReference(BindPoint);
if (Counter<=0)
{
delete BindPoint;
}
BindPoint = nullptr;
}
}
template<typename T>
T* AutoPoint<T>::operator->()
{
return BindPoint;
}
template<typename T>
AutoPoint<T>::~AutoPoint()
{
Free();
}
template<typename T>
AutoPoint<T>::AutoPoint(T* OutPoint)
{
BindPoint = OutPoint;
PointReferenceManager::Get()->AddPointReference(BindPoint);
}
template<typename T>
AutoPoint<T>::AutoPoint(AutoPoint<T>& Other)
{
if (!Other.BindPoint)
{
return;
}
BindPoint = Other.BindPoint;
PointReferenceManager::Get()->AddPointReference(BindPoint);
}
#include <iostream>
#include <map>
using namespace std;
class PointReferenceManager
{
private:
PointReferenceManager() {};
public:
static PointReferenceManager* Get();
void AddPointReference(void* Point);
int RemovePointReference(void* Point);
int GetPointReferenceCounter(void* Point);
private:
map<void*, int> CounterMap;
};
- PointReferenceManager.cpp
#include "PointReferenceManager.h"
PointReferenceManager* PointReferenceManager::Get()
{
static PointReferenceManager* Instance = new PointReferenceManager;
return Instance;
}
void PointReferenceManager::AddPointReference(void* Point)
{
auto iter = CounterMap.find(Point);
if (iter==CounterMap.end())
{
CounterMap.insert(pair<void*,int>(Point,1));
}
else
{
CounterMap[Point]++;
}
}
int PointReferenceManager::RemovePointReference(void* Point)
{
int Count = 0;
auto iter = CounterMap.find(Point);
if (iter != CounterMap.end())
{
Count = CounterMap[Point];
Count--;
if (CounterMap[Point]<=0)
{
CounterMap.erase(iter);
Count = 0;
}
else
{
CounterMap[Point] = Count;
}
}
return Count;
}
int PointReferenceManager::GetPointReferenceCounter(void* Point)
{
int Count = 0;
auto iter = CounterMap.find(Point);
if (iter!=CounterMap.end())
{
Count=CounterMap[Point];
}
return Count;
}
12.1.2 直接管理内存
int *pi = new int;
string *ps = new string;
int *pi = new int;
int *pi = new int(l024);
string *ps = new string(10, '9');
vector<int> *pv = new vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto pl = new auto(obj);
auto p2 = new auto {a, b, c};
const int *pci = new const int(1024);
const string *pcs = new const string;
- 内存耗尽,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc。
int *pl = new int;
int *p2 = new (nothrow) int;
new (nothrow)
这种形式的new为定位new。- 释放动态内存通过delete表达式,来将动态内存归还给系统。
delete p;
int i, *pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i;
delete pi1;
delete pd;
delete pd2;
delete pi2;
- 虽然一个const对象的值不能被改变,但它本身是可以被销毁的。
const int *pci = new const int(l024);
delete pci;
- 动态对象的生存期直到被释放时为止。
- 对于一个由内置指针管理的动态对象,直到被显式释放之前它都是存在的。
Foo *factory(T arg)
{
return new Foo(arg);
}
void use_factory(T arg)
{
Foo *p = factory(arg);
}
void use_factory(T arg)
{
Foo *p = factory(arg);
delete p;
}
void use_factory(T arg)
{
Foo *p = factory(arg);
return p;
}
- 动态内存的管理非常容易出错。
- 内存泄露,忘记delete内存。
- 试图操作空指针。
- 同一块内存释放两次。在delete之后,指针变成了空悬指针,指向一块曾经保存数据对象但现在已经无效的内存的指针。
12.1.3 shared_ptr和new结合使用
- 不初始化一个智能指针,它就会被初始化为一个空指针。
- 可以使用new返回的指针来初始化智能指针。
shared_ptr<double> p1;
shared_ptr<int> p2(new int(42));
- 接受指针参数的智能指针构造函数是explicit的。
shared_ptr<int> p1 = new int(1024);
shared_ptr<int> p2(new int(1024));
- 一个返回shared_ptr的函数不能在其返回语句中隐式转换。
shared_ptr<int> clone(int p)
{
return new int(p);
}
shared_ptr<int> clone(int p)
{
return shared_ptr<int>(new int(p));
}
定义和改变shared_ptr的其他方法 | 解释 |
---|
shared_ptr p(q) | p管理内置指针q所指向的对象:q必须指向new分配的内存,且能够转换为T*类型 |
shared_ptr p(u) | p从unique_ptr u那里接管了对象的所有权:将u置为空 |
shared_ptr p(q, d) | p接管了内置指针q所指向的对象的所有权,q必须能转换为T*类型,p将使用可调用对象d来代替delete |
shared_ptr p(p2, d) | p是shared_ptr p2的拷贝,唯一的区别是p将用可调用对象d来代替delete |
p.reset() | 若p是唯一指向其对象的shared_ptr,reset会释放此对象。 |
p.reset(q) | 若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空。 |
p.reset(q, d) | 若还传递了参数d,将会调用d而不是delete来释放q |
void process(shared_ptr<int> ptr)
{
}
shared_ptr<int> p(new int(42));
process(p);
int i = *p;
int *x(new int(1024));
process(x);
process(shared_ptr<int>(x));
int j = *x;
- 不要使用get初始化另一个智能指针或为智能指针赋值。
shared_ptr<int> p(new int(42));
int *q = p.get();
{
shared_ptr<int>(q);
}
int foo = *p;
- 其他shared_ptr操作,用reset来将一个新的指针赋予一个shared_ptr。
shared_ptr<int> p(new int(42));
p = new int(1024);
p.reset(new int(1024));
if (!p.unique())
{
p.reset(new string(*p));
}
*p += newVal;
12.1.4 智能指针和异常
- 如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放。
- 函数的退出有两种可能,正常处理结束或者发生了异常,无论哪种情况,局部对象都会被销毁。
void f()
{
shared_ptr<int> sp(new int(42));
}
void f()
{
int *ip = new int(42);
delete ip;
}
- 那些分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误。
struct destination;
struct connection;
connection connect(destination *);
void disconnect(connection);
void f(destination &d )
{
connection c = connect(&d);
}
- 删除器函数能够完成对shared_ptr中保存的指针进行释放的操作。
struct destination;
struct connection;
connection connect(destination *);
void disconnect(connection);
void end_connection(connection *p) { disconnect(*p); }
void f(destination &d )
{
connection c = connect(&d);
shared ptr<connection> p(&c, end_connection);
}
12.1.5 unique_ptr
- 一个unique_ptr拥有它所指向的对象,每个时刻只有一个unique_ptr指向一个给定对象。
unique_ptr<double> p1;
unique_ptr<int> p2(new int(42));
unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1);
unique_ptr<string> p3;
p3 = p2;
unique_ptr操作 | 解释 |
---|
unique_ptr 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) | 如果提供了内置指针q,令u指向这个对象 |
u.reset(nullptr) | 否则将u置为空 |
- 不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另个unique。
unique_ptr<string> p2(p1.release());
unique_ptr<string> p3(new string("Trex"));
p2.reset(p3.release());
- 调用release会切断unique_ptr和它原来管理的对象间的联系。
p2.release();
auto p = p2.release();
- 可以拷贝或赋值一个将要被销毁的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<objT, delT> p(new objT, fen);
- 重写连接程序,使用了decltype来指明函数指针类型。
void f(destination &d )
{
connection c = connect(&d);
unique ptr<connection, decltype(end connection) *> p(&c, end connection);
}
12.1.6 weak_ptr
- weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。
weak_ptr操作 | 解释 |
---|
weak_ptr w | 空weak_ptr可以指向类型为T的对象 |
weak_ptr w(sp) | 与shared_ptr sp指向相同对象的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 |
- 当创建一个weak_ptr时,要用一个shared_ptr来初始化它。
auto p = make_shared<int>(42);
weak_ptr<int> wp(p);
- 由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock,此函数检查weak_ptr指向的对象是否仍存在。
if (shared_ptr<int> np = wp.lock())
{
}
12.2 动态数组
- 标准库中包含一名为allocator的类,允许我们将分配和初始化分离,使用allocator通常会提供更好的性能和更灵活的内存管理能力。
12.2.1 new和数组
- 在下例中,new分配要求数量的对象并返回指向第一个对象的指针。
int *pia= new int[get_size()];
typedef int arrT[42];
int *p = new arrT;
- 动态数组并不是数组类型。
- 默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。
int *pia = new int[10];
int *pia2 = new int[10]();
string *psa = new string[10];
string *psa2 = new string[10]();
- 在新标准中,我们还可以提供一个元素初始化器的花括号列表。
int *pia3 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
string *psa3 = new string[10]{"a", "an", "the", string(3, 'x')};
size_t n = get_size();
int *p = new int[n];
for (int *q = p; q != p + n; ++q)
char arr[0];
char *cp = new char[0];
- 释放动态数组。
- 销毁pa指向的数组中的元素,并释放对应的内存。
- 按逆序销毁。
delete p;
delete[] pa;
typedef int arrT[42];
int *p = new arrT;
delete[] p;
unique_ptr<int[]> up(new int[10]);
up.release();
for (size_t i = 0; i != 10; ++i)
{
up[i] = i;
}
指向数组的unique_ptr | 解释 |
---|
| 指向数组的unique_ptr不支持成员访问运算符(点和箭头运算符)。 |
| 其他unique_ptr操作不变。 |
unique _ptr<T[]> u | u可以指向一个动态分配的数组,数组元素类型为T |
unique _ptr<T[]> u( p) | u指向内置指针p所指向的动态分配的数组,p必须能转换为类型T* |
u[i] | 返回u拥有的数组中位置i处的对象 |
- 与unique_ptr不同,shared_ptr不直接支持管理动态数组。
- 如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器。
shared_ptr<int> sp(new int[10], [](int *p)
{ delete[] p; });
sp.reset();
- shared_ptr不直接支持动态数组管理这一特性会影响我们如何访问数组中的元素。
- 为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素
for (size_t i = 0; i != 10; ++i)
{
*(sp.get() + i) = i;
}
12.2.2 allocator类
- 将内存分配和对象构造组合在一起会导致不必要的浪费。
- 没有默认构造函数的类就不能动态分配数组。
string *const p = new string[n];
string s;
string *q = p;
while (cin >> s && q != p + n)
{
*q++ = s;
}
const size_t size = q - p;
delete[] p;
- 标准库allocator类定义在头文件memory中,帮助我们将内存分配和对象构造分离开来。
- allocator是一个模板。
allocator<string> alloc;
auto const p = alloc.allocate(n);
标准库allocator类及其算法 | 解释 |
---|
allocator a | 定义了一个名为a的allocator对象,它可以为类型为T的对象分配内存 |
a.allocate(n) | 分配一段原始的、未构造的内存,保存n个类型为T的对象 |
a.deallocate(p, n) | 释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象,p必须是一个先前由allocate返回的指针,且n必须是p创建时所要求的大小,在此之前需调用destroy |
a.construct(p, args) | p必须是一个类型为T*的指针,指向一块原始内存,arg被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象 |
a.destroy( p) | p为T*类型的指针,此算法对p指向的对象执行析构函数 |
auto q = p;
alloc.construct(q++);
alloc.construct(q++, 10, 'c');
alloc.construct(q++, "hi");
- 还未构造对象的清况下就使用原始内存是错误的。
- 用完对象后,必须对每个构造的元素调用destroy来销毁它们。
while (q != p)
{
alloc.destroy(--q);
}
alloc.deallocate(p, n)
- 标准库还为allocator类定义了两个伴随算法,可以在未初始化内存中创建对象。
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必须指向足够大的未构造的原始内存,能够容纳给定数量的对象 |
- 分配一块比vector中元素所占用空间大一倍的动态内存,然后将原vector中的元素拷贝到前一半空间,对后一半空间用一个给定值进行填充。
auto p = alloc.allocate(vi.size() * 2);
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
uninitialized_fill_n(q, vi.size(), 42);
12.3 使用标准库:文本查询程序
12.3.1 文本查询程序设计
12.3.2 文本查询程序类的定义