0%

C++动态内存

10.动态内存

10.1 生命周期

目前的我们接触到的对象或者静态static都有着严格的生命周期:

  • 全局:程序启动时自动分配,程序结束时销毁
  • 局部对象:进入其所定义的程序时被创建,离开块时销毁
  • 静态:第一次使用前分配,程序结束时销毁

上述中的变量只使用了静态内存和栈内存。它们会自动创建和销毁。静态内存保存局部static、类static成员以及定义在任何函数之外的变量。栈内存保存定义在函数内的非static对象

除了上述的自动分配外,c++还支持动态分配对象。(其生命周期与它们在哪创建无关,只有显式的被释放时,这些对象才会被销毁)。它们被分配在内存池,称作自由空间或堆。程序用堆来存储动态分配

10.2 new动态内存

10.2.1 直接动态内存管理

c++的动态内存管理是通过一对运算符来完成:

  • new,在动态内存中为对象分配空间并返回一个指向该对象的指针
  • delete,接受一个动态对象的指针,销毁该对象,并释放关联的内存。

不再使用的动态内存应及时释放,否则会造成内存泄漏。释放delete的时机要适宜,否则在还有指针引用内存的时候释放,会导致引用非法内存的指针错误.内存泄漏:分配内存使用完毕后不释放将引起内存泄漏,会榨干内存。 相对于智能指针,直接管理内存的类与使用智能指针的类不同,他们不能依赖类拷贝、赋值和销毁操作的任何默认定义。虽然如此,但有时候我们不得不用的new与delete。在后面我们还会介绍跟高级的内存分配工具allocator类

10.2.2 使用new动态分配和初始化对象

在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针。默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值是未定义的,而类类型对象将默认构造函数进行初始化

1
2
string *ps=new string;	//初始化为空的string。类类型-->默认构造,等价于与值初始化
int *p=new int; //p指向一个动态分配、未初始化的无名对象。内置类型-->值初始化
也可以直接初始化一个动态分配的对象:
1
2
3
int *pi=new int(20);
string *ps=new string("trluper");
vector<int>* pv=new vector<int> {0,1,2,3,4,5};
注意:对于内置类型,注意值初始化和默认初始化的区别:

  • 对于定义了自己的构造函数的类类型(如string),要求值初始化是没有意义的,因为不管采用什么形式,对象都会通过默认构造函数来初始化;
  • 对于内置类型,值初始化的内置类型对象有着良好定义的值,而默认初始化的对象的值则是未定义的。
  • 对于类中那些依赖于编译器合成的默认构造函数的内置类型对象,如果它们未在类内初始化,它们的值也是未定义的

如果我们提供了一个括号包围的初始化器,就可以使用auto,此时初始化器可以推断我们想要分配的对象的类型,只有当括号中仅有单一初始化器才能使用auto

1
2
auto p1=new auto(obj);
auto p2=new auto{a,b,c}; //错误,括号中只能有单一初始化器

10.2.3 动态分片的const对象

动态分配的const对象必须进行初始化。对于定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化:

1
2
const int* pci=new const int(1024);		//显示初始化
const string *pcs=new const string; //隐式

10.2.4 内存耗尽

默认情况下,如果new不能分配所要求的内存空间,会抛出一个类型为bad_alloc的异常。我们也可以改变使用new的方式来阻止它抛出异常(称为定位new)。bad_allocnothrow都定义在头文件new中:

1
2
3

int *p1=new int;//如果分配失败,会抛出一个类型为`bad_alloc`的异常
int *p2=new(nothrow) int;//如果分配失败,new返回一个空指针

10.2.5 释放内存

delete表达式接受一个指针,指向我们想要释放的对象:delete p指针值和delete传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的。虽然一个const对象的值不能被改变,但它本身是可以被销毁的.delete条件有:

  • 应与new配对使用,既只能释放new分配得内存
  • 不要再次释放已经释放得内存
  • 如果使用new[]分配动态数组,应用delete[]释放
  • 对空指针使用delete是安全的

不需要再使用该动态分配的内存时,必须释放,否则容易内存泄漏!!以下是两个版本的use_factory函数。(p是已经new分配好的返回指针)

1
2
3
4
5
6
7
8
9
10
11
12
13
void use_factory(T *p){
//使用p
....
//函数结束,不再需要,则释放
delete p;
}

T* use_factory(T *p){
//使用p
....
//函数结束,仍然需要,则返回后由调用这释放
return p;
}

10.2.6 动态数组

某些应用需要一次性为很对对象分配内存(如vector)。为了支持这种需求,c++语言和标准库提供两者方法:

  • 分配和初始化一个对象数组
  • 应用allocator类

通常第二种法方会提供更好的性能和更灵活的管理内存能力,我们将在后面介绍,同时“STL源码剖析”会更详细。这类我们说说new[]

new和数组

1
2
3
4
5
6
7
8
9
10
11
12
int *p=new int[size];		//返回的指向第一个元素对象指针
//初始化动态分配对象的数组
//不加括号——默认初始化
int *p=new int[10]; //默认初始化
//大小之后加一对空括号——值初始化
int *p=new int[10](); //10个值初始化为0
//大小之后跟一个花括号列表——初始化器初始化
int *p=new int[10]{1,2,3,4,5,6,7,8};

//释放,使用特殊的delete来释放动态数组,在delete前加上一个空方括号对(方括号必须加上)
int *p=new int[10];
delete[] p;
当我们用new分配一个大小为0的数组时,new返回一个合法的非空指针。但此指针不能解引用——毕竟它不指向任何元素。

注意:我们得到的时数组元素的指针,而不是数组的对象,所以我们不能调用标准库函数中的begin()和end(),也不能使用范围for循环

10.2.7 placement new

placement new相当于C语言中的realloc,在已有空间的基础上,重新分配一个空间,可以不破坏原来数据,也可以把数据全部用新值覆盖。这个操作就是把已有的空间当成一个缓冲区来使用,这样子就减少了分配空间所耗费的时间,因为直接用new操作符分配内存的话,在堆中查找足够大的剩余空间速度是比较慢的。

1
2
3
4
5
6
7
//就是在指针p所指向的内存空间创建一个T1类型的对象,但是对象的内容是从T2类型的对象转换过来的,
//就是在已有空间的基础上重新调整分配的空间,类似于realloc函数
template <class T1, class T2>
inline void _construct(T1 * p, const T2& value)
{
new(p) T1(value);
}

10.2.8 三种new(重要)

new存在三种操作符,其含义和应用的场景都不同。在这里我们必须再次提到new operatoroperator newplacement new三种new。在前面我们介绍的都是具有构造效果的new operator

  • new operator指的就是new操作,使用它会经过两个步骤:一是调用::operator new操作符申请内存;二是使用类型的构造函数对内存地址进行构造。‘new operator`操作符不能被重载

    1
    classA* p=new classA(5);

  • operator new操作符是单纯的申请内存,相当于C当中的malloc函数operator new可以重载。::operator new::operator delete前面加上::表示全局,使用时就像malloc

    1
    int *tmp=(int*)(::operator new((size_t)(size*siezeof(int))));

  • placement new仅仅返回已经申请好内存的指针,它通常应用在对效率要求高的场景下,提前申请好内存,能够节省申请内存过程中耗费的时间

10.3 new operator与C的malloc的比较

首先我们来看operator new生成的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
void *p;
/* malloc (0) is unpredictable; avoid it. */
if (sz == 0)
sz = 1;
while (__builtin_expect ((p = malloc (sz)) == 0, false))//底层仍然使用malloc
{
new_handler handler = std::get_new_handler ();
if (! handler)
_GLIBCXX_THROW_OR_ABORT(bad_alloc());
handler ();
}

return p;
}
从上面可以看到operator new的内部实现仍然使用malloc,也就不奇怪new的行为像malloc

这里主要介绍new operator与malloc主要区别如下:

  • new operator分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;
  • new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。
  • new不仅分配一段内存,而且会调用构造函数,malloc不会。
  • new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。
  • new是一个操作符可以重载,内部实现仍然使用malloc这个库函数。
  • malloc分配的内存不够的时候,可以用realloc扩容。new则能使用replacement new方式来到底realloc功能
  • new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL
  • 8、申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[]delete[]多次调用析构函数,销毁数组中的每个对象。而malloc是通过free(p)来释放。

10.4 shared_ptr智能指针

为了更安全地使用动态内存,新标准库提供了两种智能指针。智能指针类似于常规指针,但区别是它负责自动释放所指向的对象:

  • shared_ptr,它允许多个指针指向同一个对象
  • unique_ptr,独占一个对象(一个指针指向一个对象)
  • weak_ptr,弱引用,指向shared_ptr所过来的对象

上述的三种类型都定义在memory头文件中

10.4.1 shared_ptr类

类似于vector,智能指针也是模板。使用该类的理由有以下几点:

  • 程序不知道自己需要使用多少对象
  • 程序不知道所需对象的准确类型
  • 程序需要在多个对象间共享数据
1
2
shared_ptr<T> p1;		//p1指向string
shared_ptr<list<T>> p2; //指向int的list

10.4.2 make_shared函数

最安全的分配和使用动态内存的方法是调用该函数。make_shared<T>(args)函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr

1
2
3
4
5
6
7
8
hared_ptr<int> p3=make_shared<int>(43);
//指向一个值为42的int的shared_ptr
shared_ptr<string> p4=make_shared<string>(10,'9');
//指向一个值为9999999999的string
share_ptr<int> p5=make_shared<int>();
//指向值初始化为0的int
auto p5=make_shared<int>();
//指向值初始化为0的int

10.4.3 shared_ptr的拷贝和赋值和释放
  • 当进行拷贝和赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。

    1
    2
    auto p=make_shared<int>(42);
    auto q(p); //q是p的拷贝,递增了的计数器。对象此时有俩引用者

  • 当给相应的shared_ptr赋予一个新值,计数器递减,当为0时,自动释放自己所管理的对象。

    1
    2
    3
    4
    5
    auto p=make_shared<int>(43);		//创建shared_ptr,为值43动态分配内存,拷贝
    p=r; //给p新赋值r,令他指向了另一个地址,此时
    //递增r所指向的引用计数
    //递减p原来的指向的对象的引用计数
    //若递减后为0,已没有引用者,自动释放

销毁\释放原理:通过一个特殊的成员函数————析构函数完成销毁工作(每个类都有一个析构函数)。析构函数一般用来来释放对象所分配的的资源shared_ptr的析构函数会递减它所指向对象的引用计数,当为0时,就会销毁对象,释放内存。

  • 当对象被销毁时,将递减其引用引用计数并检查它是否为0,如下这个例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    shared_ptr<foo> factory(T arg){
    //该函数返回shared_ptr
    return make_shared<foo>(arg)
    }
    void use_factory(T arg)
    {
    shared_ptr<Foo> p=factory(arg);
    //为arg用智能指针动态分配内存,达到能够自动释放的目的
    }//对象p离开了此作用域被销毁,此时计数减一(此例为0-->释放内存)
    void use_factory(T arg)
    {
    shared_ptr<Foo> p=factory(arg)
    //为arg用智能指针动态分配内存,达到能够自动释放的目的
    return p; //引用加1,为2
    } //此时p减一,但不为0,不释放
10.4.4 shared_ptr的共享数据

到目前为止,我们使用的类中,分配的资源都与对应对象生存期一致。当我们拷贝一个vector时,原vector副本vector中的元素是相互分离的:

1
2
3
4
5
6
vector<string> v1;
{//新作用域
vector<string> v2={"a","an","the"};
v1=v2; //从v2拷贝元素到v1
} //v2被销毁,其中的元素也被销毁
//v1依然有三个元素
所以此时的V2只是V1的一份赋值过来的值。指向的不是共同地址的数据。(这里的共同是指内存地址是同一个)。为了达到这个目的,shared_ptr就排上了用场-->多个对象共享数据。

10.4.5 定义StrBlob类

下面的是创建一个类模板(实现多个对象共享数据),每个strBlob对象设置一个shared_ptr来管理动态分配的vector。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

class StrBlob{
public:
typedef std::vector<std::string>::size_type size_type;
//默认构造函数
StrBlob();
//可变形参构造函数,内元素均为字面值且为strig
StrBlob(std::initializer_list<std::string> il);
//容器大小和判空
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
//添加和删除元素
void push_back(const std::string &t) { data->push_back; }
void pop_back();
//外部获得类的data
std::shared_ptr<std::vector<std::string>>* get(){
return data;
}
//设置data
void set(shared_ptr<std::vector<std::string>>* p){
data=p;
}
//元素访问
std::string &front();
std::string &back();

private:
//声明智能指针data
std::shared_ptr<std::vector<std::string>> data;
//如果data[i]不合法,抛出一个异常
void check(size_type i, const std::string &msg) const;
};
//构造函数
StrBlob::StrBlob():data(make_shared<vector<string>>()) {}
StrBlob::StrBlob(initializer_list<string> il):
data(make_shared<vector<string>>(il)) {}
//检查函数,i>size,抛出异常
void StrBlob::check(size_type i, const std::string &msg) const
{
if (i >= data->size())
throw out_of_range(msg);
}
//取头元素
string &StrBlob::front() const
{
check(0,"front on empty StrBlob");
return data->front();
}
//取尾元素
string& StrBlob::back() const
{
check(0,"back on empty StrBlob");
return data->back();
}
//弹出尾部元素
void StrBlob::pop_back()
{
check(0, "pop_back on empty StrBlob");
data->pop_back();
}

定义了该类后,我们创建类的对象,可以通过get函数获得智能指针,通过set赋给类的新对象即可.这样实现了类多个对象的数据共享。这要看很像类的静态成员,但它比静态成员有一个好处就是,当没有对象引用时,会释放,而不像静态成员持续到程序结束时才释放

10.4.6 shared_ptr和new结合使用

我们可以用new返回的指针来初始化智能指针。因为接受指针参数的智能指针构造函数是explicit的,因此我们不能将一个内置指针隐式转换成一个智能指针,必须使用直接初始化形式而且使用字面值:

1
2
shared_ptr<int> p1=new int(1024);    //错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024)); //正确:使用了直接初始化形式
同理,一个返回shared_ptr的函数不能在其返回语句中隐式转换成一个普通指针:
1
2
3
4
5
6
7
shared_ptr<int> clone(int p){
return new int(p); //错误:隐式转换为shared_ptr<int>
}

shared_ptr<int> clone(int p){
return shared_ptr<int>(new int(p)); //正确:显式地用int*创建shared_ptr<int>
}
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。 注意:内置指针是指内置类型(如int、char)的指针,一般没有默认构造函数。普通指针是普通类型的指针),一般有默认构造函数

10.4.7 莫交错使用new和shared_ptr

当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这么做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。如下列子:

1
2
3
4
5
6
7
void process(shared_ptr<int> p){
//空函数,离开时p对象被销毁
}
int *x(new int(24)); //危险:x是一个普通指针,而不是一个智能指针
//process(x); //错误:不能将int*转换成一个shared_ptr<int>
process(shared_ptr<int>(x)); //临时shared_ptr,合法的,但内存会被释放,引用计数变为0
int j=*x; //未定义的:x是一个空悬指针

上述代码中x是一个普通指针,当把x传给process时,报错,因为普通指针不能隐式的转换为智能指针。传入的实参显示转换为智能指针,此时x就将内存交给了shared_ptr管理,当该函数执行完毕时,该指针指针shared_ptr被销毁(x所指向的内存没了),x也就成了空悬指针。

使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。而且内置指针很可能成为空悬指针

10.4.8 智能指针和异常

使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:

1
2
3
4
5
6
7
8
9
10
void f(){
shared_ptr<int> p=make_shared<int>(43);
//如果在这里抛出了异常,其内存也会释放
}
//下面这种清空就不会释放:
void f(){
int *p=new int(43);
//如果在这里抛出了异常
delete p;
}//因为抛出了异常,无法执行delete p语句

10.4.9 删除器

某些类没有定义析构函数,此时我们可以使用shared_ptr来保证该类生成的对象的内存被正确释放,首先定义一个函数(删除器)来代替得delete。下面以连接为例子,destination类是连接信息类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//声明一些类和接口
struct destination; //该类标识我们连接的信息,如端口,地址
struct connnection; //连接类,已连接信息记录
connection connect(destionatin *p); //请求连接,返回一个连接类记录信息
void disconnection(connection); //关闭连接,内含delete操作
//对声明的类和接口定义
....
//定义删除器
void end_connecttion(connection *p){
disconnection(*p);
}
//shared_ptr使用删除
void f(destination &d){
connection c=connect(&d); //返回一个连接类信息
shared_ptr<connection> p(&c,end_connnection); //shared_ptr的用法,自定义删除器
//这样。当f函数退出时,即使是由异常引起的,Connection也会被释放关掉
}
当对象智能指针对象p离开函数f的作用域时,自动调用end_connection函数,end_connection函数又调用disconnection函数执行delete操作,注意形参时指针类型

10.4.10 shared_ptr与数组

unique_ptr不同,shared_ptr不直接支持管理动态数组。如果我们希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器(因为删除是我们默认的是delete而不是delete[]):

1
2
3
//为了使用shared_ptr,必须提供一个删除器
shared_ptr<int> sp(new int[10],[](int *p){delete [] p;});
sp.reset(); //使用我们提供的lambda释放数组,它使用delete[]
shared_ptr未定义下标运算符,而且智能指针类型不支持指针算数运算。因此,为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素:
1
2
for(size_t i=0;i!=10;++i)
*(sp.get()+i)=i; //使用get获取一个内置指针

10.5 unique_ptr

unique_ptr是C++的另一个智能指针,与shared_ptr不同的是,任何时刻,都至多只能有一unique_ptr智能指针指向一个对象,当unique_ptr指针被销毁时,其对象也被销毁。

10.5.1 unique_ptr的初始化

shared_ptr不同,没有类似make_shared的标准函数返回一个unique_ptr。因此,当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似于shared_ptr(接受参数的构造函数有explicit修饰),所以初始化unique_ptr必须采用直接初始化方式:

1
2
3
4
5
6
7
8
unique_ptr<int> p;		//定义
unique_ptr<int> p(new int(24)); //定义并初始化,指向一个值为24

//unique_ptr不支持普通的拷贝和赋值:
unique_ptr<int> p1(new int(24));
unique_ptr<int>p2(p1); //错误,不支持拷贝
unique_ptr<int>p3;
p3=p1; //或p3(p2)错误,不允许赋值

虽然我们无法拷贝或者赋值,但我们可以通过调用release或reset将指针所有权从一个(const)unique_ptr转移给另一个unique_ptr:

1
2
3
4
unique_ptr<string> p2(p1.release());	//p1转移给p2,p1置空
unique_ptr<string>p3(new string("Hello"));
//p3转移给p2ertyui
p2.reset(p3.release()); //p2释放原来的,p3被置空,p2指向的p3的
release函数会切断智能指针和它原来管理的对象的联系,它返回的指针通常用来初始化另一个智能指针或给另一个智能指针赋值。 如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:
1
auto p=p2.release();		//后面程序应该有delete(p);操作

10.5.2 向unique_ptr传递删除器

shared_ptr一样,unique_ptr默认情况(源代码)使用delete释放它指向的对象。我们可和shared_ptr一样重载一个unique_ptr中的删除器。

重载一个unique_ptr中的删除器会影响到unique_ptr类型以及如何构造(或reset)该类型的对象:我们必须在尖括号中unique_ptr指向类型之后提供删除器类型,即在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器)

1
2
3
4
5
6
7
8
9
10
//p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
//它会调用一个名为fcn的delT类型对象
unique_ptr<objT,delT> p(new objT,fcn);

void f(desitination &d){
connection c=connect(&d); //返回一个连接类信息
//当p被销毁时,连接会关闭
unique_ptr<connection,decltype(end_connection)*> p(&c,end_connnection);
//这样。当f函数退出时,即使是由异常引起的,Connection也会被释放关掉
}
注:decltype是C++11新增的一个关键字,和auto的功能一样,用来在编译时期进行自动类型推导。引入decltype是因为auto并不适用于所有的自动类型推导场景,在某些特殊情况下auto用起来很不方便,甚至压根无法使用。
1
2
auto varName=value;
decltype(exp) varName=value;

  • auto根据=右边的初始值推导出变量的类型,decltype根据exp表达式推导出变量的类型,跟``=右边的value没有关系
  • auto要求变量必须初始化,这是因为auto根据变量的初始值来推导变量类型的,如果不初始化,变量的类型也就无法推导
  • decltype不要求,因此可以写成如下形式
    1
    decltype(exp) varName;
10.5.3 指向数组的unique_ptr

标准库提供了一个可以管理new分配的数组的unique_ptr版本。使用unique_ptr管理动态数组时,我们必须在对象类型后面跟一对方括号,下面是用法介绍:

1
2
unique_ptr<int[]> up(new int[10]);
up.release(); //自动用delete[]销毁其指针

另外一方面,当一个unique_tr指向一个数组时,我们可以使用下标运算符来访问数组中的元素:

1
2
for(size_t i=0;i!=10;++i)
up[i]=i; //为每个元素赋予一个新值

10.6 weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,是弱用智能指针,它指向一个有shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变share_ptr的引用计数。 当我们创建一个weak_ptr时,我们要用以shared_ptr初始化它:

1
2
auto p=make_shared<int> (43);
weak_ptr wp(p); //wp若共享p,p的引用计数不变
由于对象可能不存在,不能直接使用weak_ptr直接访问对象,必须调用lock()函数!该函数会检查weak_ptr指向的对象是否存在,若存在,则返回一个指向共享对象的shared_ptr。如:
1
2
3
4
if(shared_ptr<int> q=wp.lock())	
{
//使用q访问对象
}

10.7 allocator类

在前面我们主要介绍了new,delete和智能指针。但他们分配的内存不是原始的,它们在分配的时候要对内存进行构造。标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。

10.7.1 allocate:分配未构造内存
1
2
allocator<string> allco;	//定义可以分配string的allocator对象
auto const p=alloc.allocate(n); //分配n个为初始化的string

alloc完成了分配n个string的连续内存的工作,并且返回一个指向这一块内存的首地址给指针p。我希望p记住这个首地址在哪免得我后边找不到了,所以把它设为const的。

10.7.2 construct:创建对象

下面我让alloc为我在这些内存上构造对象:alloc.construct(内存地址,参数......),括号里的“参数”是给我这块内存的对象类型的构造函数的参数,比如这里对于string,可以这样:

1
2
3
string *S=p;	//将首地址给S
alloc.constrcut(S,10,'A'); //该内存构造string "AAAAAAAAAA"
S++; //把内存地址往后挪,以便后续的构造
注意:使用未构造的对象的内存空间是错误
1
2
cout<<*p<<endl;		//正确,p是指向首地址
cout<<*S<<endl; //错误,还没构造

10.7.3 destroy:摧毁对象

当我们用完对象后,必须对每个构造的元素调用destory来摧毁它们。我们只能对真正构造了的元素进行destory操作,而且只有摧毁的内存或未构造的内存才能被deallocate回收。destroy参数接受一指针,对指向的对象执行析构函数:

1
2
while(q!=p)
alloc.destroy(--q);
一旦元素被销毁,我们就可以重新使用这部分内存来保存其他string,也可以将其归还给系统。释放内存通过调用dealloccate来完成:
1
alloc.dealloccate(p,n);	//p必须是allocate返回的指针,n必须是分配时指定的n

10.7.4 拷贝和填充未初始化的内存算法

allocator还有两个伴随算法,可以在未初始化内存中创建对象。它们都定义在头文件memory中。

1
2
3
4
5
6
//分配动态内存
auto p=alloc.allocate(v.size()*2);
//拷贝vi的元素到未构造内存,返回下一个未构造地址
auto q=uninitialized_copy(vi.begin(),vi.end(),p);
//将剩余空间构造为42
uninitialized_fill_n(q,vi.size(),42)