文章目录
- 第十二章 动态内存
- 12.1 动态内存与智能指针
- 12.1.1 shared_ptr类
- 12.1.2 直接管理内存
- 12.1.3 shared_ptr和new结合使用
- 12.1.4 智能指针和异常
- 12.1.5 unique_ptr
- 12.1.6 weak_ptr
- 12.2 动态数组
- 12.2.1 new和数组
- 12.2.2 allocator类
- 12.3 使用标准库:文本查询程序
- 12.3.1 文本查询程序设计
- 12.3.2 文本查询程序类的定义
第十二章 动态内存
- 目前为止使用的对象都有着严格定义的生存期。使用内存池中静态区或栈区。
- 全局对象在程序启动时分配,在程序结束时销毁。
- 对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。
- 局部static对象在第一次使用前分配,在程序结束时销毁。
- 动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。使用内存池中自由空间或堆区。
12.1 动态内存与智能指针
- new,在动态内存中为对象分配空间并返回一个指向该对象的指针。
- delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
- 动态内存的使用很容易出问题:
- 新的标准库提供了两种智能指针:
- shared_ptr允许多个指针指向同一个对象。
- unique_ptr则“独占”所指向的对象。
- weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。
- 这三种类型都定义在memory头文件中。
12.1.1 shared_ptr类
shared_ptr p1; // shared_ptr, 可以指向string
shared_ptr> p2; // shared_ptr, 可以指向int的list
//如果p1不为空,检查它是否指向一个空string
if (p1 && p1->empty())
{
*p1 = "hi"; //如果p1指向一个空string,解引用p1,将一个新值赋予string
}
| 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的标准库函数。
// 指向一个值为42的int的shared_ptr
shared_ptr p3 = make_shared(42);
// p4指向一个值为"9999999999"的string
shared_ptr p4 = make_shared(10, '9');
// p5指向一个值初始化的int,值为0
shared_ptr p5 = make_shared();
// p6指向一个动态分配的空vector
auto p6 = make_shared>();
- 当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:
auto p = make_shared(42); // p指向的对象只有p一个引用者
auto q(p); // p和q指向相同对象,此对象有两个引用者
- 每个shared_ptr都有一个关联的计数器,被称为引用计数。
- 一旦一个shared_ptr的计数器变为0,就会自动释放自己所管理的对象
r = q; //给r赋值,令它指向另一个地址
//递增q指向的对象的引用计数
//递减r原来指向的对象的引用计数
// r原来指向的对象已没有引用者,会自动释放
- shared_ptr的析构函数会递减它所指向的对象的引用计数,如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。
// factory返回一个shared_ptr,指向一个动态分配的对象
shared_ptr factory(T arg)
{
//恰当地处理arg
// shared_ptr负责释放内存
return make_shared(arg);
}
- 由于p是use_factory的局部变量,在use_factory结束时它将被销毁。
void use_factory(T arg)
{
shared_ptr p = factory(arg);
//使用p
} // p离开了作用域,它指向的内存会被自动释放掉
- 如果有其他shared_ptr也指向这块内存,它就不会被释放掉。
void use_factory(T arg)
{
shared_ptr p = factory(arg);
//使用p
return p; //当我们返回p时,引用计数进行了递增操作。
} // p离开了作用域,它指向的内存会被自动释放掉
- 程序使用动态内存出于以下三种原因之一:
- 程序不知道自己需要使用多少对象。
- 程序不知道所需对象的准确类型。
- 程序需要在多个对象间共享数据。
- 当我们拷贝一个vector时,原vector和副本vector中的元素是相互分离的。
vector v1; //空vector
{
//新作用域
vector v2 = {"a", "an", "the"};
v1 = v2; //从v2拷贝元素到v1中
}
// v2被销毁,其中的元素也被销毁
// v1有三个元素,是原来v2中元素的拷贝
- 假定希望定义一个名为Blob的类,与容器不同,希望Blob对象的不同拷贝之间共享相同的元素。
- 两个对象共享底层的数据,当某个对象被销毁时,不能单方面地销毁底层数据。
Blob b1; //空Blob
{
//新作用域
Blob b2 = {"a", "an", "the"};
b1 = b2; // b1和b2共享相同的元素
}
// b2被销毁,但b2中的元素不能销毁
// b1指向最初由b2创建的元素
- 使用动态内存的一个常见原因是允许多个对象共享相同的状态。
- 贴一个学习时老师徒手撸的简单智能指针类。
- Main.cpp
#include
#include "AutoPoint.h"
#include "PointReferenceManager.h"
using namespace std;
class A //测试用自定义类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 //测试用自定义类B
{
public:
A a;
int Num;
~B()
{
cout << "Free B" << endl;
}
};
template
class Pointer
{
public:
Pointer():BindA(nullptr){}
~Pointer()
{
if (BindA)
{
delete BindA;
}
}
public:
T* BindA;
};
A Fun()
{
A a;
return a;
}
AutoPoint GetA()
{
AutoPoint pa(new A);
pa->Num = 100;
return pa;
}
int main()
{
AutoPoint pa = GetA();
cout << pa->Num << endl;
system("Pause");
}
#pragma once
struct NULLPOINTER
{
};
template
class AutoPoint
{
public:
~AutoPoint();
AutoPoint():BindPoint(nullptr){}
explicit AutoPoint(T* OutPoint);
AutoPoint(AutoPoint& Other);
AutoPoint(AutoPoint&& Other);
T* operator->();
AutoPoint& operator=(NULLPOINTER* Point);
AutoPoint& operator=(AutoPoint& Other);//拷贝赋值运算符重载
AutoPoint& operator=(AutoPoint&& Other);
bool operator==(const AutoPoint& Other);
bool operator!=(const AutoPoint& Other);
//删除new关键字
void* operator new(size_t) = delete;
//转换规则,从当前类型到布尔的转换规则explicit禁止隐士转换
explicit operator bool()
{
return BindPoint != nullptr;
}
void Free();
public:
T* BindPoint;
};
template
AutoPoint::AutoPoint(AutoPoint&& Other)
{
if (Other.BindPoint)
{
BindPoint = Other.BindPoint;
Other.BindPoint = nullptr;
}
}
template
AutoPoint& AutoPoint::operator=(AutoPoint&& Other)
{
if (Other.BindPoint)
{
Free();
BindPoint = Other.BindPoint;
Other.BindPoint = nullptr;
}
}
template
bool AutoPoint::operator!=(const AutoPoint& Other)
{
return BindPoint != Other.BindPoint;
}
template
bool AutoPoint::operator==(const AutoPoint& Other)
{
return BindPoint == Other.BindPoint;
}
template
AutoPoint& AutoPoint::operator=(AutoPoint& Other)
{
if (!Other.BindPoint)
{
return *this;
}
//先要释放掉旧的内存引用计数
Free();
BindPoint = Other.BindPoint;
PointReferenceManager::Get()->AddPointReference(BindPoint);
return *this;
}
template
AutoPoint& AutoPoint::operator=(NULLPOINTER* Point)
{
if (!Point)
{
Free();
}
return *this;
}
template
void AutoPoint::Free()
{
if (BindPoint)
{
int Counter = PointReferenceManager::Get()->RemovePointReference(BindPoint);
if (Counter<=0)
{
delete BindPoint;
}
BindPoint = nullptr;
}
}
template
T* AutoPoint::operator->()
{
return BindPoint;
}
template
AutoPoint::~AutoPoint()
{
Free();
}
template
AutoPoint::AutoPoint(T* OutPoint)
{
BindPoint = OutPoint;
PointReferenceManager::Get()->AddPointReference(BindPoint);
}
template
AutoPoint::AutoPoint(AutoPoint& Other)
{
if (!Other.BindPoint)
{
return;
}
BindPoint = Other.BindPoint;
PointReferenceManager::Get()->AddPointReference(BindPoint);
}
#include
#include
- 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(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; // pi指向一个动态分配的、未初始化的无名对象
string *ps = new string; //初始化为空string
int *pi = new int; // pi指向一个未初始化的int
int *pi = new int(l024); // pi指向的对象的值为1024
string *ps = new string(10, '9'); /)
{
//获得一个连接;记住使用完后要关闭它
connection c = connect(&d);
//使用连接
//如果我们在f退出前忘记调用disconnect, 就元法关闭c了
}
- 删除器函数能够完成对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 p(&c, end_connection);
//使用连接
//当f退出时(即使是由于异常而退出),connec巨on会被正确关闭
}
12.1.5 unique_ptr
- 一个unique_ptr拥有它所指向的对象,每个时刻只有一个unique_ptr指向一个给定对象。
unique_ptr p1; //可以指向一个double的unique_ptr
unique_ptr p2(new int(42)); // p2指向一个值为42的int
unique_ptr p1(new string("Stegosaurus"));
unique_ptr p2(p1); //错误:unique_ptr不支持拷贝
unique_ptr p3;
p3 = p2; //错误:unique_ptr不支持赋值
| unique_ptr操作 | 解释 |
|---|
| unique_ptr u1 | 空unique_ptr,可以指向类型为T的对象,u1会使用delete来释放它的指针 |
| unique_ptr u2 | u2会使用一个类型为D的可调用对象来释放它的指针 |
| unique_ptr 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。
//将所有权从p1(指向string Stegosaurus)转移给p2
unique_ptr p2(p1.release()); // release将p1置为空
unique_ptr p3(new string("Trex"));
//将所有权从p3转移给p2
p2.reset(p3.release()); // reset释放了p2原来指向的内存
- 调用release会切断unique_ptr和它原来管理的对象间的联系。
p2.release(); //错误:p2不会释放内存,而且我们丢失了指针
auto p = p2.release(); //正确,但我们必须记得delete(p)
- 可以拷贝或赋值一个将要被销毁的unique_ptr。
- 对于两段代码,编译器都知道要返回的对象将要被销毁,在此情况下,编译器执行一种特殊的拷贝。
unique_ptr clone(int p)
{
//正确:从int*创建一个unique_ptr
return unique_ptr(new int(p));
}
unique_ptr clone(int p)
{
unique_ptr ret(new int(p));
// ...
return ret;
}
// p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
//它会调用一个名为fcn的delT类型对象
unique_ptr p(new objT, fen);
- 重写连接程序,使用了decltype来指明函数指针类型。
void f(destination &d )
{
connection c = connect(&d); //打开连接
//当p被销毁时,连接将会关闭
unique ptr p(&c, end connection);
//使用连接
//当f退出时(即使是由于异常而退出),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(42);
weak_ptr wp(p); // wp弱共享p,p的引用计数未改变
- 由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock,此函数检查weak_ptr指向的对象是否仍存在。
if (shared_ptr np = wp.lock()) //如果np不为空则条件成立
{
//在if中,np与p共享对象
}
12.2 动态数组
- 标准库中包含一名为allocator的类,允许我们将分配和初始化分离,使用allocator通常会提供更好的性能和更灵活的内存管理能力。
12.2.1 new和数组
- 在下例中,new分配要求数量的对象并返回指向第一个对象的指针。
//调用get_size确定分配多少个int
int *pia= new int[get_size()];//pia指向第一个int
typedef int arrT[42]; // arrT表示42个int的数组类型
int *p = new arrT; //分配一个42个int的数组,p指向笫一个int
- 动态数组并不是数组类型。
- 默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。
int *pia = new int[10]; // 10个未初始化的int
int *pia2 = new int[10](); // 10个值初始化为0的int
string *psa = new string[10]; // 10个空string
string *psa2 = new string[10](); // 10个空string
- 在新标准中,我们还可以提供一个元素初始化器的花括号列表。
// 10个int分别用列表中对应的初始化器初始化
int *pia3 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// 10个string,前4个用给定的初始化器初始化,剩余的进行值初始化
string *psa3 = new string[10]{"a", "an", "the", string(3, 'x')};
size_t n = get_size(); // get_size返回需要的元素的数目
int *p = new int[n]; //分配数组保存元素
for (int *q = p; q != p + n; ++q)
char arr[0]; //错误:不能定义长度为0的数组
char *cp = new char[0]; //正确:但cp不能解引用
- 释放动态数组。
- 销毁pa指向的数组中的元素,并释放对应的内存。
- 按逆序销毁。
delete p; // p必须指向一个动态分配的对象或为空
delete[] pa; // pa必须指向一个动态分配的数组或为空
typedef int arrT[42]; // arrT表示42个int的数组类型
int *p = new arrT; //分配一个42个int的数组,p指向笫一个int
delete[] p; //方括号是必需的,因为我们当初分配的是一个数组
// up指向一个包含10个未初始化int的数组
unique_ptr up(new int[10]);
up.release(); //自动用delete[]销毁其指针
for (size_t i = 0; i != 10; ++i)
{
up[i] = i; //为每个元素赋子一个新值
}
| 指向数组的unique_ptr | 解释 |
|---|
| 指向数组的unique_ptr不支持成员访问运算符(点和箭头运算符)。 |
| 其他unique_ptr操作不变。 |
| unique _ptr u | u可以指向一个动态分配的数组,数组元素类型为T |
| unique _ptr u( p) | u指向内置指针p所指向的动态分配的数组,p必须能转换为类型T* |
| u[i] | 返回u拥有的数组中位置i处的对象 |
- 与unique_ptr不同,shared_ptr不直接支持管理动态数组。
- 如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器。
//为了使用shared_ptr, 必须提供一个删除器
shared_ptr sp(new int[10], [](int *p)
{ delete[] p; });
sp.reset(); //使用我们提供的lambda释放数组,它使用delete[]
- shared_ptr不直接支持动态数组管理这一特性会影响我们如何访问数组中的元素。
- 为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素
// shared_ptr未定义下标运算符,并且不支持指针的算术运算
for (size_t i = 0; i != 10; ++i)
{
*(sp.get() + i) = i; //使用get获取一个内置指针
}
12.2.2 allocator类
- 将内存分配和对象构造组合在一起会导致不必要的浪费。
- 没有默认构造函数的类就不能动态分配数组。
string *const p = new string[n];
//构造n个空string
string s;
string *q = p; // q指向笫一个string
while (cin >> s && q != p + n)
{
*q++ = s; //赋予*q一个新值
}
const size_t size = q - p; //记住我们读取了多少个string
//使用数组
delete[] p; // p指向一个数组;记得用delete[]来释放
- 标准库allocator类定义在头文件memory中,帮助我们将内存分配和对象构造分离开来。
- allocator是一个模板。
allocator alloc; //可以分配string的allocator对象
auto const p = alloc.allocate(n); //分配n个未初始化的string
| 标准库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; // q指向最后构造的元素之后的位置
alloc.construct(q++); // *q为空宇符串
alloc.construct(q++, 10, 'c'); // *q为cccccccccc
alloc.construct(q++, "hi"); // *q为hi!
- 还未构造对象的清况下就使用原始内存是错误的。
- 用完对象后,必须对每个构造的元素调用destroy来销毁它们。
while (q != p)
{
alloc.destroy(--q); //释放我们真正构造的string
}
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中的元素拷贝到前一半空间,对后一半空间用一个给定值进行填充。
//分配比vi中元素所占用空间大一倍的动态内存
auto p = alloc.allocate(vi.size() * 2);
//通过拷贝vi中的元素来构造从p开始的元素
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
//将剩余元素初始化为42
uninitialized_fill_n(q, vi.size(), 42);
12.3 使用标准库:文本查询程序
12.3.1 文本查询程序设计
12.3.2 文本查询程序类的定义