[C++]动态内存

动态内存和智能指针

静态内存:局部static对象,类static数据成员,定义在任何函数之外的变量

栈内存:保存定义在函数内的非static对象

堆(自由空间):动态分配的对象

头文件:memory

智能指针,自动的释放所指向的对象

shared_ptr:允许多个指针指向同一个对象

unique_ptr:独占所指向的对象

weak_ptr:一种弱引用,指向shared_ptr所管理的对象


shared_ptr类

shared_ptr<string> p1;
shared_ptr<vector<int>> p2;

使用前进行条件判断,保证p1不是指针,p1指向了一个空string

shared_ptr<string> p1;
if(p1 && p1 -> empty())
{
    *p1 = "hello";
}

shared_ptr和unique_ptr都支持的操作

p -> mem;    //获取内部成员
p.get();     //返回p中保存的指针,若智能指针释放了其对象,返回的指针所指向的对象也就消失了
swap(p,q);
p.swap(q)    //交换p,q

shared_ptr独有的操作

make_shared<T>(args);    //返回一个shared_ptr,指向一个动态分配的类型为T的对象,用args初始化它
shared_ptr<T>p(q);       //p是shared_ptr q的拷贝,会增加q中的计数器
p = q;                   //保证指针能够相互转换,此操作会递减p的引用计数,递增q的引用计数;p的引用计数为0会被删除
p.unique();              //p.use_count() == 1
p.use_count();           //返回与p共享对象的智能指针数量,很慢,用于调试

通常使用make_shared函数分配使用,若没传递任何参数,会默认进行值初始化

拷贝和赋值的过程本质上都是计数器调整

shared_prt会调用析构函数来销毁对象

可以使用函数返回shared_ptr类型,return语句会向此函数的调用者返回一个拷贝,此时原对象随着函数的销毁而被销毁,但此时引用计数值并不为零,所以对应内存还会保留。

shared_ptr<int> shaptr_test(int x)
{
    auto t = make_shared<int>(x);
    return t;
}
int main()
{
    shared_ptr<int> p1 = shaptr_test(5);
    cout << *p1 << endl;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
}

所以,注意要保证在无用之后没有保留,否则会浪费内存。尤其容易忽略的是,把shared_ptr保存在vector中,对vector重排后,忘记erase掉无用元素。


使用动态内存的原因

(1)程序不知道自己需要使用多少对象:vector

(2)程序不知道所需对象的准确类型

(3)程序需要在多个对象间共享内存

在类中定义share_ptr成员,实现多个对象间的共享内存


直接管理内存

运算符new, delete

int *i = new int(1024);
int *s = new string(5, '9');
vector<int> *v = new vector<int>{0,1,2};
int *i2 = new int();    //值初始化,*i2为0
int *i3 = new int;      //默认初始化,*i3的值未定义

对于定义了自己的构造函数的类,值初始化都会通过默认的构造函数来初始化;对于内置类型才会有区别。

使用auto自动判断

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

用new分配const对象

const int *p = new const int(10);

虽然不能改变const动态对象,但可以用delete来销毁

对于定义了默认构造函数的类类型,其const对象可以隐式初始化,其它则必须显式初始化

内存耗尽时,new会抛出std::bad_alloc异常

使用定位new传递额外参数nothrow,当分配失败时,返回一个空指针

int *p = new (nothow) int;

delete之后最好重置指针

在delete之后,很多机器上指针仍然保存着(已经释放了的)动态内存的地址,此时指针变成了空悬指针。所以,在指针要离开作用域前要释放它关联的内存,如果要保留指针,可以在delete之后重置为nullptr

只是有限的保护,若有多个指针指向同一地址则很难检测到。


new与shared_ptr结合使用

可以用new初始化shared_ptr

shared_ptr<int> p(new int(35));

接受指针参数的智能指针构造函数是explicit的,故不能讲一个内置指针隐式转换为智能指针,必须通过显式的绑定

定义和改变shared_ptr的方法

tr《T

shared_ptr<T> p(q);    //p管理内置指针q所指向的对象,q必须指向new分配的内存,且能够转换为T*类型
shared_ptr<T> p(u);    //p从unique_ptr u处接管对象的所有权,u被置空
shared_ptr<T> p(q, d); //p接管了q所指向对象的所有权,使用可调用对象d代替delete
shared_ptr<T> p(p2,d); //p是shared_ptr p2的拷贝,区别是p将用可调用对象d来代替delete
p.reset();           
p.reset(q);
p.reset(q, d);
//若p是唯一指向对象的shared_ptr,reset会释放,或另p指向q,参数d用于提供delete操作

普通指针和智能指针混合使用易产生的错误

void process(shared_ptr<int> ptr)
{
/*……*/
}
/*……*/
int *p(new int(100));
process(shared_ptr<int>(p));
int j = *p;

将临时的shared_ptr传入到process后,当调用结束时,临时对象将被销毁,引用次数变为0,p变成空悬指针,此时使用p的行为是未定义的。

当将一个shared_ptr绑定到一个普通指针时,相当于管理责任的移交,最好不要再使用内置指针。

同样的,shared_ptr定义了一个get函数,返回一个内置指针,应用场景是:向不能使用智能指针的代码传递一个内置指针,这里需要注意不要被delete。特别是,永远不要用get初始化另一个智能指针

在改变底层对象之前,检查自己是否是当前对象仅有的用户

if(!p.unique())
    q.reset(new int(*p));
*p += val;


智能指针和异常

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

内置指针则做不到。

对于那些没有定义析构函数的类,通常要求用户显式地释放所使用的任何资源,这时如果使用shared_ptr来避免内存泄露会默认的使用delete。但对于有些资源并不适用,比如网络连接,这时可以采用自定义的函数来代替delete。

void end_connection(connection *p) { disconnect(*p); }
/* ……*/
void f(destination &d)
{
    connection c = connect(&d);
    shared_ptr<connection> p (&c, end_connection);
    /* ……*/
}

此时,即便是由于异常而退出,也可以保证connection会被正确光比


unique_ptr

定义

只能将其绑定到一个new返回的指针上

unique_ptr<int> p1;
unique_ptr<int> p2(new int(10));

unique_ptr的对象是独有的,所以不支持普通的拷贝或赋值

//使用类型为D的可调用对象d代替delete
unique_ptr<T> u1;
unique_ptr<T, D> u2;
unique_ptr<T, D> u(d);
u = nullptr;    //释放并置空
u.release();    //放弃控制权,返回当前保存的指针,置空
//释放原对象,将u指向q的对象
u.reset();
u.reset(q);
u.reset(nullptr);

仅使用release不会释放内存,而且会丢失原指针,如:p.release()


传递和返回unique_ptr

不能拷贝unique_ptr的规则有一个例外:可以拷贝或赋值一个将要被销毁的unique_ptr。

典型的例子是从函数返回一个unique_ptr



auto_ptr

auto_ptr有拷贝语义,拷贝后源对象变得无效;unique_ptr则无拷贝语义,但提供了移动语义

auto_ptr不可作为容器元素,unique_ptr可以作为容器元素

auto_ptr不可指向动态数组(尽管不会报错,但不会表现出正确行为),unique_ptr可以指向动态数组


weak_ptr

不控制所指向对象生存期的智能指针,指向一个由shared_ptr管理的对象,不改变shared_ptr的引用计数,一旦shared_ptr被销毁,weak_ptr没有任何阻碍的效果。

使用方法

weak_ptr<T> w;
weak_ptr<T> w(sp);    //与shared_ptr sp指向相同对象
w = p;
w.reset();            //w置空
w.use_count();
w.expired();          //w.use_count() == 0
w.lock();             //若expired为true,返回空shared_ptr,否则返回一个指向w对象的shared_ptr

访问前注意判定

if(shared_ptr<int> sp = w.lock())
{
    /*……*/
}


动态数组

一次为很多对象分配内存

new表达式,allocator类

更多的情况还是使用容器


new和数组

int *p = new int[get_size()];    //p指向第一个int

方括号中的大小必须是整型,但不必是常量

由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end

关于初始化

int *pi = new int[10];    //10个未初始化
int *pi = new int[10]();    //10个值初始化

还可以使用列表初始化,不能用auto分配数组

有趣的是,可以动态分配一个空数组,使用时除了不能解引用可以进行其它操作,类似于一个尾后迭代器

char arr[0];               //错误
char *cp = new char[0];    //正确,但不能解引用

释放数组

delete [] arr;

即使是使用别名来定义一个数组,释放时也必须加上[],但是在vs下测试可以不加上[]

typedef int arr[10];    //arr为10个int的数组的类型别名
int *p = new arr();
delete []p;             //方括号理论上必须

顺序是按照驻足中元素的逆序


智能指针和动态数组

可以管理new分配的数组的unique_ptr版本

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

可以使用下标直接访问

如果希望使用shared_ptr管理一个动态数组,必须提供一个删除器

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

传递给一个lambda作为删除器。若未提供删除器,默认情况shared_ptr使用delete来销毁对象,这与释放动态数组时没有加[]产生的问题一样。

shared_ptr未定义下标运算符,且不支持指针的算术运算,所以访问只能通过get之后再加减(vs下不支持)


allocator类

将内存分配和构造对象分离,避免浪费。

头文件memory

是一个模板,分配的内存是原始的、未构造的。

基本操作

allocator<T> a;
a.allocate(n);        //分配内存,保存n个类型为T的对象,返回起始位置指针
//释放从指针p出开始的内存,共保存了n个对象,p必须是先前由allocate返回的指针,n必须为当初构造时的大小
a.deallocate(p, n);
//args被传递给T的构造函数,用于在p指向的内存中构造一个对象
a.construct(p ,args);
a.destory(p);        //对p指向的对象析构

分配内存

allocator<string> alloc;
auto p = alloc.allocate(10);
auto q = p;
alloc.construct(p++);                //空字符串
alloc.construct(p++, 5, 'h');        //5个h
alloc.construct(p++, 'hello');

早期版本中不能采用其它构造函数来构造一个元素,只有两个参数:指针,参数类型的值

逐个destory,但内存并没有被释放

while(q != p)
{
    alloc.destory(--p);
}

拷贝和填充

uninitialized_copy(b, e, b2);    //从迭代器b和e间拷贝元素到迭代器b2指定的未构造的原始内存中,假定b2足够大
uninitialized_copy(b, n, b2);    //拷贝n个元素uninitialized_fill(b, e, t);     //在迭代器b和e指定的原始内存范围中创建对象,值为t的拷贝
uninitialized_fill(b, n, t);     //拷贝n个,假定足够大


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章