C++常见问题总结_动态内存与智能指针

动态内存与智能指针

静态内存用于保存局部static对象、类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。分配在静态内存或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。
程序用堆来存储动态分配的对象(在程序运行时分配的对象)。动态对象的生存周期由程序来控制,当动态对象不再使用时,我们的代码必须显示销毁他们。

直接管理内存
通过new分配内存,delete释放new分配的内存。相对于智能指针,使用这两个运算符管理内存容易出错。而且自己直接管理内存的类与使用智能指针的类不同,他们不能依赖对象拷贝、赋值和销毁的任何默认定义。

1、使用new动态分配和初始化对象。
默认情况下,动态分配的对象是默认初始化的,这意味着对于内置类型和组合类型的对象的值将是未定义的,而对于类类型对象将用默认构造函数进行初始化。

int*pi=new int(1024);
string *ps=new string(10,'9');
vector<int>*pv=new vector<int>{0,1,2,3,4,5,6,7,8,9};

//也可以对动态分配的对象进行值初始化。
string *ps1=new string;//默认初始化为空
string *ps=new string();//值初始化为空
int *pi1=new int;//默认初始化;*pi1的值未定义
int *pi2=new int();//值初始化为0

//使用auto
auto p1=new auto(obj);//p指向一个与obj类型相同的对象,用obj初始化。
auto p2=new auto{a,b,c};//错误,只能含有单一的初始化器

动态分配的const对象
类似其他任何const对象,一个动态分配的const对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const对象可以是隐式初始化,而其他类型的对象就必须现实初始化。返回的是一个指向const的指针。

const int*pci=new const int(1024);
const string *pcs=new const string;

2、定位new
定位new允许我们向new传递额外的参数,例如:默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。可以改变使用new的方式来阻止抛出异常。

int *p2=new(nothrow) int;//分配失败,返回一个空指针

3、释放动态内存delete

delete p;

我们传递给delete的指针必须是指向动态分配的内存,或是一个空指针。两个动作:销毁给定指针指向的对象并释放对应内存。

int i,*pi1=&i,*pi2=nullptr;
double *pd=new double(33),*pd2=pd;
delete i; //错误
delete pi1;//未定义
delete pd;//正确
delete pd2;//未定义:内存已经被释放了
delete pi2;//正确

释放一块非动态内存,或者将相同的指针释放多次其行为是未定义的。

example:

void us(T arg)
{
    foo *p=process(arg);
    delete p; //此处不释放,动态内存将无法释放。
    return;
}

说明:与类类型不同,当一个内置类型对象被销毁时什么也不会发生。当一个内置指针离开其作用域时,它所指向的对象什么也不会发生。动态内存不会自动释放。

4、使用new和delete管理动态内存常见问题


  • 忘记delete内存。会导致内存泄漏。
  • 使用已经释放掉的内存。通常在释放内存之后将指针置为空,来检测这种错误。
  • 同一块内存被释放两次,自由空间可能被破坏。

智能指针
标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似于普通指针,不过他负责自动释放所指向的对象。

#include<memory>
//属于模板类
1shared_ptr ;//允许多个指针指向同一个对象
2、unique_ptr ;//独占所指向的对象
3、weak_ptr   ;//一种弱引用,指向shared_ptr所管理的对象
  • shared_ptr类

智能指针(shared_ptr和unique_ptr都支持的操作)所支持的操作:

1、创建一个智能指针

shared_ptr<T> sp;
unique_ptr<T> up;
//example:
shared_ptr<string> p1;
shared_ptr<list<string>>p2;

2、使用智能指针

//与普通指针类似
if(p1&&p1->empty())
    *p1="hi";

3、其他的操作

/*返回p中保存的指针(为一种内置指针),
若智能指针释放了其对象,
返回的指针所指向的内存就释放了*/
p.get();

//交换p和q中的指针
swap(p,q);

p.swap(q);

智能指针shared_ptr独有的操作:

make_shared<T>(args);
shared_ptr<T>p(q); 
p=q;
p.unique(); //p.use_count()为1则返回true
p.use_count(); //返回p共享对象的智能指针数量

1、make_shared函数
make_shared(args);返回一个动态分配的类型为T的对象。使用args初始化此对象。

shared_ptr<int>p3=make_shared<int>(42);
shared_ptr<string>p4=make_shared<string>(10,'9');
shared_ptr<int>p5=make_shared<int>();//指出华int 即0。

make_shared用其参数来构造给定类型的对象。对于p4,传递的参数必须与string的某个构造函数匹配。对于p5传递的参数必须能用来初始化一个int,以此类推。当我们不传递参数时,对象就会进行值初始化。

2、shared_ptr的拷贝和赋值

auto p=make_shared<int>(42);
auto q(p);

我们可以认为每个shared_ptr都有一个关联的计数器,称为引用计数。
只要拷贝一个shared_ptr时 他的引用计数会增加。当给shared_ptr赋值时或者销毁时计数就会减。
shared_ptr的析构函数会递减它的引用计数。如果引用计数变为0,就会销毁对象,并释放他所占的内存。

auto r=make_shard<int>(42);//只有一个引用者
r=q;
//递增q指向的对象的引用计数
//递减r原来指向对象的引用计数
//r原来指向的对象没有引用者,会自动释放

定义和改变shared_ptr的其他方法:

shared_ptr<T> p(q);

//p从unique_ptr u 那里接管对象所有权 ,将u置空
shared_ptr<T> p(u);

//p管理内置指针q所指向的对象,将调用可调用对象d来代替delete(这里没要求q指向new分配的内存)
shared_ptr<T>p(q,d);

//p是智能指针p2的拷贝,唯一区别是p将用可调用对象d来代替delete
shared_ptr<T> p(p2,d);

/*1、(无参数版本)若p是唯一指向其对象的shared_ptr,会释放此对象。
  2、若传递了参数内置指针q,令p指向q。
  3、参数d,来代替delete
*/
p.reset();
p.reset(q);
p.reset(q,d);

//example:
p=new int(1024);//错误
p.reset(new int(1024));//p指向一个新对象 类似赋值

//与unique混合使用
if(!p.unique())
    p.reset(new string(*p));//不是唯一用户;分配新的拷贝
*p+=newval;//现在是唯一用户,可以改变了。

1、使用new返回的指针初始化智能指针

shared_ptr<T>p(q); 

上述指针q必须指向new分配的内存,因为智能指针默认使用它所关联的对象。并且可以转换成T*类型。p管理内置指针q指向的内存。

example1:

//只能使用直接初始化,智能指针接收指针参数的构造函数时explicit的。
shared_ptr<int> p1=new int(1024);//错误
shared_ptr<int>p2(new int(1024));//正确

shared_ptr<int> clone(int p)
{
    return new int(p);//错误
    return shared_ptr<int>(new int(p));//正确
}

example2:

/*混合使用普通指针与智能指针带来的问题*/
void process(shared_ptr<int> ptr)
{
    //使用ptr
}
int *x(new int(1024))
process(x);//错误
process(shared_ptr<int>(x));//正确,但是内存在process作用域之后会被释放掉
int j=*x;//未定义,x为空悬指针
/*正确使用:
shared_ptr<int>p(new int(42));
process(p); 拷贝p会增加引用计数
int i=*p;
*/

note:当我们讲一个shared_ptr绑定到一个普通指针时,我们将内存管理的责任就交给智能指针,不应该在使用内置指针访问。

example3:

/*使用get的返回值初始化另一个智能指针或赋值,要注意的问题*/
    shared_ptr<int>p(new int(1024));
    int *q=p.get();
    { //新作用域
        shared_ptr<int>(q);//两个不同的智能指针指向了相同的动态内存
    } //q被销毁,指向的内存被释放
    int foo=*p;//未定义:指向的内存被释放了  
//此外,当p被销毁时,这块内存会被delete第二次。

智能指针和异常
使用异常处理的程序能在异常发生后令程序流程继续,这种程序要求在异常发生后资源能被正确地释放。

void f()
{
    shared_ptr<int> sp(new int(42));
    //这时抛出了一个异常,且在f中没有捕获
}//在函数结束时,shared_ptr自动释放内存。

不论是正常处理结束还是发生了异常,在sp被销毁时会检查引用计数,当发现引用计数为0时,内存会被释放掉。

void f()
{
    int *ip=new int(42);
    //这时抛出了一个异常,且在f中未捕获
    delete ip;
}

如果在new和delete之间发生了一个异常,且异常没有在f中捕获,则内存永远不会被释放掉。因为,ip是指向此内存的唯一指针。

examle:

/*管理不具有良好定义的析构函数的类*/
//例如使用一个网络库
struct destination;  //表示我们正在连接什么
struct connection;   //使用连接所需要的信息
connection connect(destination*);//打开连接
void disconnect(connection);  //关闭给定连接

void f(destination &d)
{
    //获得一个连接,使用完后要关闭它
    connection c=connect(&d);
    //使用连接
    //退出前,忘记调用disconnect,就无法关闭c。
}

/*使用shared_ptr来管理一个connection,需要定义一个函数来代替delete操作*/

void end_connection(connection *p){disconnect(*p);}

void f(destination &d)
{
    connection c=connect(&d);
    shared_ptr<connection> p(&c,end_connection);
    //使用连接
    //connection 会被正常关闭
}

当p被销毁时,他不会对自己保存的指针执行delete,而是调用end_connection。

unique_ptr类
一个unique_ptr拥有它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定的对象。当unique_ptr被销毁时,它所指向的对象也会被销毁。
当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上(同样为explicit的)

unique_ptr<string> p1(new string(" asd"));
unique_ptr<string> p2(p1);//错误
unique_ptr<string> p3;
p3=p1//错误

由于一个unique_ptr独占它所指的对象,所以不支持普通的拷贝和赋值操作。
但我们可以拷贝或赋值一个将要被销毁的unique_ptr。如:

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

//返回一个局部对象的拷贝
unique_ptr<int> clone(int p)
{
    unique_ptr ret<int>(new int(p));
    return ret;
}

unique_ptr所特有的操作:

unique_ptr<T> u1; //空unique_ptr
unique_ptr<T,D> u2; //使用类型为D的可调用对象释放指针
unique_ptr<T,D> u3(d);//用类型为D的对象d代替delete

u=nullptr;//释放u所指向的对象
u.release();//放弃对指针的控制权 并且返回指针,并将u置为空

u.reset();//释放u所指对象
u.reset(q);//如果提供了内置指针q,让u指向这个对象;否则置为空。
u.reset(nullptr);

1、使用release或reset
我们可以使用release或reset将指针的所有权从一个(非const)unique_ptr转移给另外一个unique。

    unique_ptr<string>p2(p1.release());//所有权转移给p2 p1置空
    unique_ptr<string> p3(new string(" asd"));
    p2.reset(p3.release());//释放了原来p2的内存 将p3的控制权转移给p2

    p.release();//错误 不会释放内存 但我们丢失了指针
    auto p2=p.release();//正确必须记得delete(p)

2、向unique传递删除器

//p指向一个类型为objT的对象,
//并且使用一个类型为delT的对象释放objT对象
//它会调用一个名为fcn的delT类型的对象。
unique_ptr<objT,delT>p(new objT,fcn)

//例如对于我们的连接操作
void end_connection(connection *p){disconnect(*p);}

void f(destination &d)
{
    connection c=connect(&d);
    unique_ptr<connection,decltype(end_connection)*> p(&c,end_connection);
    //使用连接
    //connection 会被正常关闭
}

weak_ptr类
weak_ptr是一种不控制所指向对象生存周期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,还是会被释放。是一种“弱”共享。
weak_ptr支持的操作:

weak_ptr<T> W ;//空的
weak_ptr<t>W(sp);//sp为shared_ptr,T必须可以转换成sp指向的类型。
w=p;//p可以为shared_ptr或者weak_ptr w与p共享对象

w.reset();//w置空

w.use_count();//与w共享对象shared_ptr的数量
w.expired();//w.use_count()为0true
w.lock();//如果expired为true返回一个空的shared_ptr,否则返回一个指向w的对象的shared_ptr

当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它,并且由于对象有可能不存在,我们不能直接使用weak_ptr直接访问对象,必须调用lock。检查weak_ptr指向的对象是否存在。
example:

    auto p=make_shared<int>(42);
    weak_ptr<int> wp(p);
    if(shared_ptr<int>np=wp.lock()) //np不为空条件成立
    {
        //在if中,np与p共享对象
    }

猜你喜欢

转载自blog.csdn.net/xc13212777631/article/details/80511100