blueyi's notes

Follow Excellence,Success will chase you!

0%

C++之动态内存管理

动态内存与智能指针

1.静态内存用来保存局部static对象,类static数据成员以及定义在任何函数之外的变量。static对象会在第一次使用前分配,程序结束时销毁。栈内存用来保存定义在函数内的非static对象。栈对象仅在其定义的程序块运行时才存在。分配在静态内存和栈内存中的对象由编译器自动创建和销毁。

2.每个程序拥有的内存池即自由空间(free store)或堆(heap)可以由程序控制用于存储动态分配(dynamically allocate)的对象。

3.动态定义的变量使用的是默认初始化方式,如果变量是含有默认构造函数的类型,将会调用默认构造函数进行初始化,例如string *ps = new string()中ps指向的内存将被默认初始化为空字符串,因为string的默认构造函数会将其初始化为空字符串。如果是内置变量它指向的将是未初始化的内存空间,也就是说它的值将是任意的。虽然默认情况下,内置变量的默认初始化是由其定义的位置决定的,但对于动态分配的内存,其值都将不被初始化,如int *pi = new int;,不管是否将其放在任意函数体的外部,其中pi指向的内存都将是未定义的,虽然当将其放在全局时pi本身是个已经初始化的指针。但int *pi2 = new int()将会调用值初始化来将pi2指向的内存初始化0。

4.动态内存的管理是通过一对运算符new和delete来完成,new在动态内存中为对象分配空间并返回一个指向该对象的指针,delete接受一个动态对象的指针,销毁该对象并释放相应的内存。

shared_ptr类

5.C++11中提供了两个智能指针,它们都是模板类:shared_ptr允许多个指针指向同一个对象,unique_ptr则独占所指向的对象。另外还有一个weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。它们都定义在头文件memory中。

6.默认初始化的智能指针中保存着一个空指针,如果在一个条件判断中使用智能指针,效果就是检测它是否为空。eg:

1
2
3
shared_ptr<string> p1;  //定义一个可以指向string的shared_ptr
if (p1 && p1->empty()) //如果p1不为空,并检查它是否指向一个空string
*p1 = "hi";

7.最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并初始它,返回指向此对象的shared_ptr。该函数是一个函数模板,所以使用是需要提供类型。类似容器的emplace成员,make_shared用相应的参数来构造给定类型的对象,所以传递给make_shared的参数必须与想要构造的对象的类型的某个构造函数相匹配,如果不传递任何参数,对象就会进行值初始化。eg:

1
2
3
shared_ptr<int> p3 = make_shared<int>(42);  //指向一个值为42的int的shared_ptr
shared_ptr<string> p4 = make_shared<string>(10, '9'); //p4指向一个值为"9999999999"的string
shared_ptr<int> p5 = make_shared<int>(); //p5指向一个使用值初始化的int,值为0

8.每个shared_ptr都关联着一个引用计数器(reference count),当拷贝一个shared_ptr时(例如使用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数,以及作为函数的返回值),计算器都会递增。当给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开作用域)时,计算器会递减。所以当赋值号两边都是shared_ptr时,则左边shared_ptr关联的计算器会被递减,右边会递增。eg:

1
2
3
4
5
auto r = make_shared<int>(42);  //r指向的int只有一个引用者
r = q; //给r赋值,令它指向另一个地址
//递增q指向的对象的引用计数
//递减r原来指向的对象的引用计数
//如果r原来指向的对象已没有引用者,会自动释放

9.如果将shared_ptr存放于一个容器中,而后不再需要全部元素,而是只使用其中一部分,要记得用erase删除不再需要的那些元素。否则该部分内存将会一直被占用。

10.使用动态内存通常出于以下三种原因:

  • 1.程序不知道自己需要使用多少对象
  • 2.程序不知道所需对象的准备类型
  • 3.程序需要在多个对象间共享数据

new和delete

1.默认情况下,动态分配的对象是默认初始化的,这意味着使用new创建的内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。可以使用直接初始化方式或C++11中的列表初始化方式在创建时初始化一个动态分配的对象:

1
2
3
4
5
6
7
8
string *ps = new string;  //默认初始化为空string
string *ps1 = new string(); //值初始化为空string
int *pi1 = new int; //默认初始化,p1指向一个未初始化的int
int *pi2 = new int(); //值初始化为0,p2指向一个值为0的int
int *pi3 = new int(42); //pi指向的对象的值为42
string *ps3 = new string(10, '9'); //*ps的值为9999999999
vector<int> *pv = new vector<int>{0, 1, 2, 3, 4}; //pv指向含有5个int值的vector,值分别为0到4
vector<int> *pv1 = new vector<int>(); //pv1指向一个空vector,当然可以向pv1中添加push_back值

2.C++11中支持使用auto来自动推断new创建的类型:

1
auto p1 = new auto(obj);  //p指向一个与obj类型相同的对象,该对象用obj进行初始化

3.可以动态分配const对象:

1
const int *pci = new const int(1024);

4.默认情况下如果自由空间已经被耗尽,使用new分配失败时会抛出bad_alloc,可以增加nothrow参数来禁止抛出异常,而是返回一个空指针。bad_alloc和nothrow都定义在头文件new中。eg:

1
2
int *p1 = new int;  //如果分配失败,new抛出std::bad_alloc
int *p1 = new (nothrow) int; //如果分配失败,new返回一个空指针

5.释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为都是未定义的。但可以释放一个const动态对象。

6.由内置指针(而非智能指针)管理的动态内存在被显式释放前一直都会存在,所以在函数内创建的局部指针变量,在离开函数前一定要释放,因为一旦离开函数,该指针变量将不可访问,也就无法再释放分配的动态内存。

7.动态内存常见出错原因:

  • 忘记delete内存
  • 使用已经释放掉的对象
  • 同一块内存释放两次

坚持只使用智能指针即可避免这些问题

8.对于delete之后的指针,该指针值会变成无效指针,但该指针变量本身依然存在,且它指向一个无效的内存地址,该指针成为空悬指针(dangling pointer),可以通过将nullptr赋给空悬指针来避免由于引用该指针而引起的非法内存地址访问问题。这种方式只提供了非常有限的保护,最好还是使用智能指针。

9.shared_ptr可以和new结合使用,由于接受指针参数的智能指针构造函数是explicit的,所以不能将一个内置指针隐匿转换为一个智能指针,必须使用直接初始化形式。eg:

1
2
3
4
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<int>
shared_ptr<int> clone(int p) { return shared_ptr<int>(new int(p)); //正确:显式地调用int*创建shared_ptr<int>

10.当将一个shared_ptr绑定到new创建普通指针时,就将内存管理的责任交给了shared_ptr,我们不应该再使用内置指针来访问shared_ptr所指向的内存。

10.shared_ptr的get()函数可以返回一个内置指针,此函数仅在我们需要向不能使用智能指针的代码传递一个内置指针时使用,使用get返回的指针的代码不能delete些指针。永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。

21.shared_ptr的reset()函数和unique函数通常一起使用来控制多个shared_ptr共享的对象。eg:

1
2
3
if (!p.unique())
p.reset(new string(*p)); //我们不是唯一用户,分配新的拷贝
*p += newVal; //现在我们知道自己是唯一的用户,可以改变对象的值

22.函数的退出有两可能:正常处理结束或者发生了异常,无论哪种情况,由于局部对象都会被销毁,所以使用智能指针管理动态内存时内存都会被释放掉,但使用内置指针在对应的delete之前内存不会被释放。

23.shared_ptr默认是使用delete来释放需要被释放的动态内存。可以在初始化shared_ptr为其传递一个删除器(deleter)来完成对不是new分配的内存的释放。eg:

1
2
3
4
5
6
void end_connection(connection *p) {disconnect(*p);}
void f(destination &d)
{
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
}

24.智能指针陷阱:

  • 不使用相同的内置指针值初始(或reset)多个智能指针
  • 不delete get()返回的指针
  • 不使用get()初始化或reset另一个智能指针
  • 如果使用get()返回的指针,记住当最后一个对应的智能指针销毁后,该指针就变为无效了。
  • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

unique_ptr

25.一个unique_ptr“拥有”它所指向的对象,与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定的对象,当unique_ptr被销毁时,它所指向的对象也被销毁。且unique_ptr没有类似make_shared的标准库函数返回一个unique_ptr,只能通过将其绑定到一个new返回的指针上,且必须采用直接初始化形式:

1
2
unique_ptr<string> p1;  //可以指向一个string的unique_ptr
unique_ptr<string> p2(new string("hi")); //p2指向一个值为"hi"的string

26.由于unique_ptr拥有它指向的对象,所以它不支持普通拷贝或赋值操作,但可以通过调用release和reset来将指针所有权从一个(非const)unique_ptr转移给另一个unique。eg:

1
2
3
unique_ptr<string> p2(p1.release()); //release将p1置为空,并将所有权由p1转移给p2
unique_ptr<string> p3(new string("hello"));
p2.reset(p3.release()); //reset释放了p2原来指向的内存,并将p3原拥有的所有权转移给p2

27.不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr,最常见的就是从函数返回一个unique_ptr:

1
2
3
4
5
6
7
8
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;
}

28.可以向unique_ptr传递删除器,但它的删除器必须在尖括号中提供删除器类型,形如:unique_ptr<objT, delT> p (new objT, fcn);

weak_ptr

29.weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数,一旦最后一个指向对象的shared_ptr被销毁,则对象就会被释放,与weak_ptr是否指向它无关。创建weak_ptr时需要使用shared_ptr来初始化它,weak_ptr支持将一个shared_ptr赋值给一个weak_ptr:

1
2
3
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); //wp弱共享p;p的引用计数未改变
weak_ptr<int> wp1 = p;

30.weak_ptr通常用于核查类,相当于可以放置一个类来监视当前shared_ptr的引用状态,因为它不会对shared_ptr所指向对象的生命周期产生影响。weak_ptr除了支持使用shared_ptr初始化,以及赋值外操作外,还支持以下操作:

  • 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的使用详解http://stackoverflow.com/questions/12030650/when-is-stdweak-ptr-useful

动态数组

1.大多数应用应该使用标准库容器而不是动态分配的数组,使用容器更为简单、更不容易出错,并且可能有更好的性能。

new和数组

1.使用new分配要求数量的对象并返回指向第一个对象的指针:int *pa = new int[get_size()];
方括号中的大小必须是整型,但不必是常量。
分配一个数组会得到一个元素类型的指针,该指针指向所分配的数组的第一个元素,并不是返回一个数组类型的对象。

2.默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以使用如下3种方式对数组进行初始化:

1
2
3
4
5
6
int *pi = new int[10];  //10个未初始化的int
int *pi2 = new int[10](); //10个值初始化为0的int
string *ps = new string[10]; //10个空string
string *ps2 = new string[10](); //10个空string
//C++11支持的初始化方式
int *pi3 = new int[10]{0,1,2,3}; //10个int,前4个使用列表初始化,剩下的使用值初始化

如果初始化列表的元素数目大于数组元素数目,new会失败,且不分配任何内存
不能使用auto分配数组

3.动态分配一个空数组是合法的。即当我们用new分配一个大小为0的数组时,new返回一个合法的非空指针,此指针与其他任何指针都不同,类似于尾后指针,可以像使用尾后指针一样使用它,但不能对它解引用。

4.释放动态数组使用delete [] p;,中括号不能省略,delete一个数组指针时忘了方括号,或者在delete一个单一对象时使用了方括号,行为都是未定义的。

5.标准库中的unique_ptr可以与new配合使用来管理动态数组,unique_ptr声明时必须在对象后面跟一对空方括号,unique_ptr指向数组时,不支持成员访问运算符(点和箭头),毕竟unique_ptr指向的是一个数组而不是单个对象。但支持下标操作。 :

1
2
3
4
unique_ptr<int[]> up(new int[10]);  //up指向一个包含10个未初始化int的数组  
up.release(); //自动使用delete[]销毁其指针。
for (size_t i = 0; i != 10; ++i)
up[i] = i; //为每个元素赋予一个新值

6.当使用shared_ptr与new一起使用时,由于shared_ptr不支持直接管理动态数组,所以必须提供自已定义的删除器,且不支持下标操作,只能使用get获取一个内置指针:

1
2
3
4
shared_ptr<int[]> sp(new int[10], [](int *p){ delete[] p; }); 
sp.reset(); //会使用提供的lambda释放数组,即delete[]
for (size_t i = 0; i != 10; ++i)
*(sp.get() + i) = i; //使用get获取一个内置指针

allocator类

使用allocator类来管理动态内存更方便,速度可能会更快。
1.标准库allocator类定义在头文件memory中,是一个模板类,它帮助我们将内存分配和对象构造分享开来,它提供一种类型感知的内存分配方法,它分配的内存是原始的,未构造的。allocator定义的方式:

1
2
allocator<string> alloc;                //可以分配string的allocator对象
auto const p = alloc.allocate(n); //分配n个未初始化的string

2.allocator支持的操作:

  • allocator a //定义一个名为a的allocator对象,它可以为类型为T的对象分配内存
  • a.allocate(n) //分配一段原始的、未构造的内存,保存n个类型为T的对象
  • a.deallocate(p, n) //释放T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是一个先前由allocate返回的指针,且n必须是p创建时所要求的大小。在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destroy。
  • a.construct(p, args) //p必须是一个类型为T*的指针,指向一块原始内存。arg将被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象
  • a.destroy(p) //p为T*类型的指针,该函数对p指向的对象执行析构函数

3.使用allocator的典型方式是:

  • 1.声明指定类型的allocator对象alloc
  • 2.使用alloc调用alloc.allocate(n)函数分配可以创建n个指定对象的原始内存
  • 3.使用alloc调用alloc.construct(q, args)在原始内存中构造对象
  • 4.使用alloc对每个对象调用alloc.destroy(p)来销毁它们
  • 5.使用alloc调用alloc.deallocate(p, n)释放内存,p必须指向所分配内存的第一个位置

使用未构造的内存,其行为是未定义的

1
2
3
4
5
6
7
8
9
10
allocator<string> alloc;                //可以分配string的allocator对象
auto const p = alloc.allocate(n); //分配n个未初始化的string
auto q = p; //q始终指向最后构造的元素之后的位置
alloc.construct(q++); //*q为空字符串
alloc.construct(q++, 10, 'c'); //*q为ccccccccc
alloc.construct(q++, "hi"); //*q为hi
/*使用该内存中的对象*/
while (q != p)
alloc.destroy(--q); //释放我们真正构造的string
alloc.deallocate(p, n); //释放内存,将其归还给系统

4.标准库为allocator类定义了两个伴随算法,他们可以拷贝和填充未初始的内存。分别是uninitialized_copy(begin, end, b2),uninitialized_copy_n(begin, n, b2) 和 uninitialized_fill(begin, end, t),uninitialized_fill_n(begin, n, t)。
一次uninitialized_copy的调用会返回一个指针,该指针指向最后一个构造的元素之后的位置。

Welcome to my other publishing channels