朱琛的小屋

c++ primer 笔记第十二弹 - 动态内存

c++ primer中第十二章:动态内存的笔记。

内存类型分类

  1. 静态内存:静态内存用来保存局部static对象,类static数据成员,以及定义在任何函数之外的变量。
  2. 栈内存:用来保存在函数内的非static对象,由编译器进行自动创建和销毁。
  3. 堆内存:又称自由空间。是我们在程序运行时分配的对象。动态对象的生存期由程序来控制。所以需要我们显式的销毁。

直接管理内存 -newdelete

  1. 如果new无法分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。
  2. new的作用是分配所需内存,返回指向该对象的指针
  3. delete的作用是销毁给定指针指向的对象,释放对应内存。
  4. new是可以分配const对象的。
  5. 动态内存不同于之前的shared_str,他不会自动释放,所以它的生存期一直到被显式释放之前。但是在很多情况下,我们都容易忘记释放内存,导致内存泄露。

直接管理内存的劣势:

  1. 忘记delete内存。

  2. 使用已经释放过的对象。

  3. 同一块内存释放两次。

  4. 特别是我们有多个指针指向同一块内存,当其中一个指针delete后,其他指针指向的内存也没有了,此时所有指针无效了。但是我们容易忘记该事实。

智能指针

智能指针是c++11新标准颁布的,用来管理动态内存。智能指针与以前的new,delete区别主要在于他可以自动释放所指向的对象。主要有两类智能指针:shared_ptrunique_strshared_ptr允许多个指针指向同一个对象,unique_str独占所指向的对象。

shared_ptr

shared_ptr也是一种模板,定义时我们需要提供指针可以指向的类型:

1
2
shared_ptr<string> p1;    //可以指向string类型
shared_ptr<list<int>> p2; //可以指向list<int>类型

shared_ptr分配内存

shared_ptr使用make_shared标准库函数来分配内存。

1
2
3
4
5
6
// shared_ptr that points to an int with value 42
shared_ptr<int> p3 = make_shared<int>(42);
// p4 points to a string with value 9999999999
shared_ptr<string> p4 = make_shared<string>(10, ’9’);
// p5 points to an int that is value initialized (§ 3.3.1 (p. 98)) to 0
shared_ptr<int> p5 = make_shared<int>();

注意一点,调用make_shared<string>时传递的参数必须与string的某个构造函数相匹配。如果不传递任何参数,对象会自己进行初始化。

我们也可以使用c++11新标准的auto关键字进行定义

1
auto p6 = make_shared<vector<string>>();

shared_ptr拷贝与赋值

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

每一个shared_ptr都有一个关联的计数器。当我们拷贝一个shared_str时,计数器就会递增。

基本上一个例子我们就能明白。

1
2
3
4
5
auto r = make_shared<int>(42); // r指向的int只有一个引用者
r = q; // r指向了q所指向的地址
// 递增q所指向对象的引用计数
// 递减r原来所指向对象的引用计数
// r原来所指向对象的引用计数为0,即没有引用者,自动释放

shared_str自动销毁所指对象

shared_str是通过一个特殊的成员函数-析构函数来完成销毁工作的。如果当引用计数为0时,它的析构函数就会自动销毁对象,释放内存。

shared_str自定义删除器

如果我们希望被销毁时,对指针不是单纯的进行delete操作,我们可以定义一个自己的删除器函数传给智能指针。

1
2
3
4
5
6
7
8
9
void end_connection(connection *p) { disconnect(*p); }

void f(destination &d /* other parameters */)
{

connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
// use the connection
// when f exits, even if by an exception, the connection will be properly closed
}

我们可以看到上面的例子,当p被销毁时,我们会调用end_connection,来确保链接被关闭。这样即使在代码运行中发生异常,由于智能指针的特质,连接也一定会被关闭。

unique_str

unique_strshared_str不同的是,某个时刻只能有一个unique_str指向一个给定对象。当unique_str被销毁时,它所指向的对象也会被销毁。

unique_ptr分配内存

unique_str没有make_shared这样的标准库函数来返回动态指针。我们只能使用new进行直接初始化。

1
2
unique_ptr<double> p1; 
unique_ptr<int> p2(new int(42));

unique_ptr拷贝与赋值

因为unique_str绑定了他指向的对象,所以她不能进行普通的拷贝或者赋值。我们只能调用release或者reset来转移指针的所有权。

1
2
3
4
5
// transfers ownership from p1 (which points to the string Stegosaurus) to p2
unique_ptr<string> p2(p1.release()); // release makes p1 null
unique_ptr<string> p3(new string("Trex"));
// transfers ownership from p3 to p2
p2.reset(p3.release()); // reset deletes the memory to which p2 had pointed

我们可以发现,利用release将切断unique_ptr和他所绑定对象的关系,返回的值可以用来初始化另一个unique_ptr。这样就达到了所有权转移的目的。

unique_str自定义删除器

unique_str自定义删除器和上面shared_str不同,我们必须在尖括号中unique_str指向类型后提供删除器类型。重写上面shared_str自定义删除器的例子:

1
2
3
4
5
6
7
8
void f(destination &d )
{

connection c = connect(&d);
// 当p被销毁时,连接将会被关闭
unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);

// 即使发生异常,连接也会被关闭
}

在这个例子中,decltype返回的是一个函数类型,所以我们必须添加一个* 来指出我们该类型的一个指针。

unique_str管理动态数组

unique_str初始化动态数组的时候,我们要在对象类型后加一对空方括号。

1
2
3
// up 指向一个包含10个非初始化int的数组
unique_ptr<int[]> up(new int[10]);
up.release(); // 自动调用delete[]销毁其指针

当一个unique_str指向一个数组时,我们可以使用下标运算符来访问数组中的元素。

1
2
for (size_t i = 0; i != 10; ++i)
up[i] = i;

allocator

new操作将内存分配和对象构造结合在一起,delete将对象析构和内存释放组合在一起。但是有的时候,我们希望将内存分配和对象构造分离。我们可以先分配大块内存,然后再真正需要时才执行对象创建操作。

allocator分配内存

allocator是一个模板,为了定义一个对象,我们要指明这个allocator可以分配的对象类型。当一个allocator对象分配内存时,它会根据对象类型来确定恰当的内存大小。但是这些内存是未构造的,所以我们需要的时候,要构造相应的对象。

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

allocator构造对象

我们可以使用construct来在未分配的内存初始化构造的对象。

1
2
3
4
auto q = p; // q 指向最后构造的元素之后的位置
alloc.construct(q++); // *q is the empty string
alloc.construct(q++, 10, ’c’); // *q is cccccccccc
alloc.construct(q++, "hi"); // *q is hi!

allocator销毁对象

当我们用完对象后,我们需要销毁这个对象,对指向的对象执行析构函数。

1
2
while (q != p)
alloc.destroy(--q);

我们需要用一个while循环,来不停的销毁对象,并且对q进行递减操作。这样能够保证所有构造元素都被销毁。销毁后,我们可以重新使用这部分内存来保存其他的string

allocator销毁内存

1
alloc.deallocate(p, n);

我们传递给该函数的指针不能为空,必须指向由allocate分配的内存。而且传递给deallocate的大小参数必须和allocate分配内存时的大小一样。

朱琛 wechat
扫一扫,用手机看更方便~
坚持原创技术分享,您的支持将鼓励我继续创作!