C++智能指针和动态内存

文章目录

  • 动态内存
    • 程序使用动态内存的三种情况:
    • 直接内存管理
      • 动态内存分配
  • 智能指针
    • std::share_ptr< >
      • make_shared函数
      • 引用计数(reference count)
      • shared_ptr和new结合使用
      • shared_ptr指向其他资源——通过智能指针管理其他资源
    • std::unique_ptr< >
    • std::weak_ptr< >
  • 动态数组
    • 智能指针与动态数组
  • allocator类

动态内存

全局变量在程序启动时分配,在程序结束时销毁;局部自动变量在进入函数块时被创建,在离开函数块时被销毁;static自动变量在第一次使用之前分配,在程序结束时销毁。智能指针一般被用来管理动态申请的内存,保证动态内存能够被自动地、安全地释放。

程序使用动态内存的三种情况:

  • 程序不知道自己需要使用多少对象
  • 程序不知道对象的准确类型
  • 程序需要在多个对象之间共享数据

直接内存管理

C语言内存管理
使用 malloc/calloc(在stdlib.h中) 等分配内存的函数时,一定要检查其返回值是否为“空指针”(亦即检查分配内存的操作是否成功),这是良好的编程习惯,也是编写可靠程序所必需的。但是,如果你简单地把这一招应用到 new 上,那可就不一定正确了。我经常看到类似这样的代码:

    int* p = new int[SIZE];
    if ( p == 0 ) // 检查 p 是否空指针
        return -1;
    // 其它代码

其实,这里的 if ( p == 0 ) 完全是没啥意义的。C++ 里,如果 new 分配内存失败,默认是抛出异常的。
所以,如果分配成功,p == 0 就绝对不会成立;而如果分配失败了,也不会执行 if ( p == 0 ),
因为分配失败时,new 就会抛出异常跳过后面的代码。
如果你想检查 new 是否成功,应该捕捉异常:
try {
        int* p = new int[SIZE];
        // 其它代码
    } catch ( const bad_alloc& e ) {
        return -1;
    }

虽然可以采用另一种方式:int* p = new (std::nothrow) int; 这样如果 new 失败了,就不会抛出异常,而是返回空指针。但是无法保证"new (std::nothrow) ClassName"这样的表达式不会抛出exception。所以,慎用nothrow new。

动态内存分配

在C++中,通过new运算符为对象分配动态空间,new会返回在一个指向对象的对象类型的指针(无须转换指针类型),我们还可以在分配空间的同时初始化空间;通过delete运算符销毁对象空间,它接受一个new返回的指针。使用智能指针的类与自己直接管理内存的类不同,一般不需要定义构造、拷贝、赋值、析构等函数。

int *p1 = new int; //默认初始化,即未初始化
int *ptr(new int(12));
int *p2 = new string;
int *p3 = new string(); 
int *p = new int(); //值初始化
vector<int> *p4 = new vector<int>{0, 1, 2}; //列表初始化

动态分配const对象

const int *p = new const int(1024);
const string *p = new const string; 

内存耗尽
当内存耗尽时,new表达式就会失败,并抛出一个类型为std::bad_alloc的异常。

#include//包含bad_alloc和nothrow
int *p = new (nothrow) int; //阻止抛出异常,如果分配失败,则返回NULL

delete
释放new运算符申请的空间。

智能指针

  • 确保发生异常时,也能正确释放内存

在C++11中,有3种智能指针,都定义在memory头文件中,分别是:shared_ptr< type >、unique_ptr< type >和weak_ptr< type >。这3个指针都是模板类,因此在在创建智能指针时,必须提供额外的信息——指针指向的对象的类型。

std::share_ptr< >

shared_ptr允许多个共享指针同时指向同一个对象。

shared_ptr<string> p1;		//指向string, 默认初始化的智能指针保存着一个空指针
shared_ptr<list<int>> p2;	//指向int的list
if(p1 && p1->empty())		//在条件语句中使用智能指针,用以检查指针是否为空
	*p1 = "hi";				//解引用智能指针
					
shared_ptr和unique_ptr都支持的操作
shared_ptr< T > sp 空智能指针,指向类型为T的对象
unique_ptr< t > 空智能指针
if ( p ) 将p用作条件判断;如果p指向一个对象,则返回true
*p 解引用p
p->mem 等价于(*p).mem
p.get() 返回p中保存的指针
swap(p,q)或p.swap(q)
shared_ptr独有的操作
make_shared< T > ( args) 该函数返回一个shared_ptr指针,类型为T的对象用args初始化,这是最安全的分配和使用动态内存的方法
shared_ptr< T > p(q) 拷贝初始化
p=q 接受一个指针参数的构造函数是explicit的,因此q必须是shared_ptr类型,不能是new返回的内置指针
p.unique() 如果p.use_count()=1,则返回true
p.use_count() 返回与p共享对象的智能指针的数量

make_shared函数

make_shared是一个模板函数,此函数在动态内存中分配一个对象,并初始化它,然后返回一个指向该对象的shared_ptr。

#include
//指向一个值为12的int。
shared_ptr<int> p1 = make_shared<int>(12);

//传递的参数必须与string的某个构造函数匹配,如果不提供参数,将进行值初始化。
auto p2 = make_shared<string>(10,'9');

引用计数(reference count)

无论何时,我们拷贝一个shared_ptr,计数器都会递增。

  • 用一个shared_ptr初始化(或赋值)另一个;
  • 将它作为一个实参传递给函数;
  • 作为函数的返回值。

计数器何时会递减:

  • 给shared_ptr赋予新值,即让该shared_ptr指向其他内存;
  • shared_ptr被销毁。

容器的erase()成员函数会删除元素,进而调用元素的析构函数。

shared_ptr和new结合使用

shared_ptr p = new int(10) //错误
shared_ptr p(new int(10)) //OK

shared_ptr的接受一个指针参数的构造函数是explicit的,因此不能将一个内置指针隐式的转换为shared_ptr,必须使用直接初始化形式。
下面的语句也是错误的:

shared_ptr<int> clone(int p)
{
	return new int(p); //错误:无法隐式转换
}

shared_ptr指向其他资源——通过智能指针管理其他资源

一般情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为shared_ptr默认使用delete来释放它所关联的对象。如果我们想让shared_ptr指向其他资源,则必须提供自己的操作来代替delete。

shared_ptr p(q, d) p接管了内置指针q所指对象的所有权。p将使用可调用对象d来代替delete
p.reset() 或 p = nullptr 使p指向空,并将引用计数减一
p.reset(q) 使p指向q
p.reset(q, d) 使p指向q,并用可调用对象d代替delete

不要将同一块内存绑定到多个独立创建的shared_ptr上,即多个指向通一块内存的shared_ptr应该是通过智能指针之间的copy创建的。也就是说不要使用同一个内置指针来创建多个shared_ptr。

std::unique_ptr< >

与shared_ptr<>不同,某一时刻只能有一个unique_ptr可以指向一个给定对象。因此,unique_ptr不支持普通的拷贝与复制操作。当unique_ptr被销毁时,它指向的对象也会被销毁。
但是我们可以拷贝和赋值一个将要被销毁的unique_ptr。也就是说支持移动拷贝和移动赋值。
创建unique_ptr:

unique_ptr<int> p1(new int(10)); //OK
unique_ptr<int> p2(p1); //error
unique_ptr<int> p3;
p3 = p1; //error

p3 = unique_ptr<int>(new int(9))//移动赋值

unique_ptr<int> clone()
{
	return unique_ptr<int>(new int(12));
}

unique_ptr<int> clone2()
{
	unique_ptr<int> p(new int(90));
	return p;
}
unique_pte u(d) 空unique_ptr,指向类型为T的对象,并用类型为D的对象d来代替delete
u = nullptr 释放u所指对象,并将u置空
u.release() u放弃对对象的控制权,并返回指针,u置为空
u.reset() 释放u所指对象
u.reset(q)
u.reset(nullptr)

std::weak_ptr< >

weak_ptr不会控制所指对象的生存周期,它指向一个由shared_ptr管理的对象。weak_ptr不会改变shared_ptr的引用计数。因此,weak_ptr所指对象可能已经被释放,所以必能直接通过weak_ptr来直接访问所指对象,必须调用lock()。
weak_ptr可以阻止用户访问不存在的对象。
if(shared_ptr sp = wp.lock()) //如果sp不为空,则可以访问
{

}

weak_ptr w(sp) 与shared_ptr指向相同对象的weak_ptr
w = q q可以是shared_ptr或weak_ptr
w.expired() 若w.use_count()为0,则返回true
w.lock() 如果expire为true,返回一个空的shared_ptr;否则返回一个指向w的对象的shared_ptr

动态数组

使用容器可以不必关心内存释放问题,因此可以使用默认析构等。
对于动态数组,也不能使用范围for语句。

string *p = new string[10]{"a", "ab"}; //初始化器

char int[0] //error
char *p = new char[0]; //OK,但无法解引用p。

智能指针与动态数组

使用智能指针管理动态数组的两种方法:

方法一、特殊的指向动态数组的unique_ptr

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

//此时可以使用up[i]来访问元素。

方法二、使用shared_ptr(p, d):

shared_ptr<int> sp(new int[10], [](int *p){delete[] p;});

for(int i = 0; i != 10; ++i)
	*(sp.get() + i) = i; //shared_ptr不支持下标运算符

allocator类

C++中std::allocator的使用
包含在memory中,将内存分配与对象构造分离开来。分配原始的,未构造的内存。

/*allocator是模板,未定义对象,必须指明可分配对象的类型*/
allocator<string> alloc; 

//在使用allocator对象分配内存时,可能会考虑内存对其;不会调用string的构造函数。
//n可以为0
auto const p = alloc.allocate(n);

alloc.destroy(p) //p必须指向alloc分配的内存。不能为空。 一次析构一个对象

//p不能为空,n为申请的空间的个数。即只能一次性释放所有的内存;
//并且在释放之前,必须先调用destroy()来析构对象。
alloc.deallocate(p, n) 
allocator a
a.allocate(n)

你可能感兴趣的:(C++)