引言
程序使用三种不同的内存
静态内存:static成员以及任何定义在函数之外的变量栈内存:一般局部变量堆内存(自由空间):动态分配的对象
静态内存和栈内存中的变量由编译器产生和销毁,动态分配的对象在我们不再使用它时要由程序员显式地销毁
一、介绍
动态分配内存
- new():为对象分配空间,并返回指向该对象的指针
- delete:销毁对象,并释放与之相关的内存
使用智能指针:定义在头文件memory中
- shared_ptr:允许多个指针指向同一个对象
- unique_ptr:“独占”所使用的对象
- weak_ptr:伴随类,弱引用,指向shared_ptr所管理的对象
和容器一样,只能指针也是一种模板,需要给它传入一个参数来指定类型
二、shared_ptr类
声明shared_ptr:
shared_ptr<string> p1; //shared_ptr,可以指向string
shared_ptr<list<int>> p2; //shared_ptr,可以指向list<int>
使用方式与普通指针一致,解引用返回它所指向的对象,在条件表达式中检查是否为空
//若p1不为空且指向一个空string
if(p1 && p1->empty()){
*p1 = "hi"; //对p1重新赋值
}
make_shared函数
make_shared<typename>(arguments)
在动态内存中分配并初始化一个对象
返回指向此对象的shared_ptr指针
//指向一个值为42的int的shared_ptr
shared_ptr<int> p1 = make_shared<int>(42);
//指向一个值为"999"的string的shared_ptr
shared_ptr<string> p2 = make_shared<string>(3, '9');
//指向一个值为0的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>();
没有传入参数时,进行值初始化
auto p4 = make_shared<string>(); //p4指向空string
shared_ptr的拷贝和引用
每个share_ptr都有一个关联的计数器
当拷贝shared_ptr时,计数器会递增
当shared_ptr被赋予新值或者shared_ptr被销毁(如一个局部的shared_ptr离开其作用域),计数器会递减
当一个shared_ptr的计数器==0时,内存会被释放
auto r = make_shared<int>(42);
r = q; //给r赋值,使它指向另一个地址
//递增q指向的对象的引用计数
//递减r指向的对象的引用计数
//如果计数器为0,自动释放
shared_ptr自动销毁所管理的对象…
和其他类一样,shared_ptr类型也有析构函数
shared_ptr的析构函数会
- 递减指针所指向的对象的引用计数
- 当对象的引用计数为0时,销毁对象并释放内存…shared_ptr还会自动释放相关联对象的内存
举例:
//factory返回一个share_ptr,指向一个动态分配的对象
shared_ptr<Foo> factory(T arg){
//对arg的操作
return make_shared<Foo>(arg);
}
void ues_factory(T arg){
shared_ptr<Foo> p = factory(arg);
//使用p
}
//p离开了作用域,由于引用计数由1减到0,对象被销毁,内存释放
如果有其他引用计数也指向该对象,则对象内存不会被释放掉
//factory和上述一致
//ues_factory返回shared_ptr的拷贝
void use_factory(T arg){
shared_ptr<Foo> p = factory(arg);
//使用p
return p; //返回p的拷贝,此时递增了计数器,引用数为2
}//p离开作用域,对象计数器引用2-1=1,对象内存没有释放
return shared_ptr时,如果不是返回引用类型,则会进行拷贝,shared_ptr的计数器+1后-1,最终shared的计数器不变
由于在最后一个shared _ptr销毁前内存都不会释放,保证shared_ptr在无用之后不再保留就非常重要了。如果你忘记了销毁程序不再需要的shared_ptr,程序仍会正确执行,但会浪费内存。
share_ptr 在无用之后仍然保留的一种可能情况是,你将shared _ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素。在这种情况下,你应该确保用erase删除那些不再需要的shared_ptr元素。
如果你将shared ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。
使用动态生存期的资源的类
程序使用动态内存的三种原因
- 程序不知道自己需要使用多少对象
- 不知道所需对象的准确类型
- 需要在多个对象间共享数据
容器类常出于第一种原因使用动态内存,在15章会看见出于第二种原因的例子,本节讨论第三种原因
先考虑这么一种情况:
我们要定义一个Blob类,当该类型的对象拷贝时,对象共享底层数据。
如b2 = b1时,b2,b1共享底层数据,对b2的操作也会印象到b1,且销毁b2时,b1的仍指向原数据
Blob<string> b1; //空Blob
{
//新作用域
Blob<string> b2 = {"a","an","the"};
b1 = b2; //b1和b2共享数据
}//b2离开作用域,被销毁了,但b2的数据不能被销毁
//b1指向b2的原数据
应用举例:Blob类
定义Blob类
最终,我们希望将Blob定义为一个模板类,但现在我们先将其定义为StrBlob,即底层数据是vector<string>的Blob
class StrBlob{
public:
//拷贝控制
StrBlob();//默认构造函数
StrBlob(initializer_list<string> il); //列表初始化
StrBlob(const StrBlob& strb);
//查询
int size() const {return data->size();}
bool empty() const {return data->empty();}
//添加和删除元素
void push_back(const string &t) {data->push_back(t);}
void pop_back() {data->pop_back();}
//访问元素
string& front();
string& back();
private:
shared_ptr<vector<string>> data;
//如果data[i]不合法,抛出异常
void check(int i, const string &msg) const;
};
StrBlob的构造函数
StrBlob::StrBlob() : data(make_shared<vector<string>>())
{cout<<"in StrBlob dafault"<<endl;};
StrBlob::StrBlob(initializer_list<string> il) :
data(make_shared<vector<string>>(il))
{cout<<"in StrBlob initializer_list"<<endl;}
元素访问成员函数
在访问时必须保证容器非空,定义check函数进行检查
void StrBlob::check(int i, const string& msg) const{
if(i >= data->size())
throw out_of_range(msg);
}
元素访问成员函数:
string& StrBlob::front(){
//如果vector为空,check会抛出一个异常
check(0, "front on empty StrBlob");
return data->front();
}
string& StrBlob::back(){
check(0, "back on empty StrBlob");
return data->back();
}
StrBlob的拷贝、赋值和销毁
StrBlob使用默认的拷贝、赋值和析构函数对此类型的对象进行操作
当我们对StrBlob对象进行拷贝、赋值和销毁时,它的shared_ptr成员也会默认地进行拷贝、赋值和销毁
//由于data是private的
//在StrBlob中设置一个接口look_data
//look_data返回data的引用
class StrBlob{
public:
//...
shared_ptr<vector<string>>& look_data()
{return data;} //返回引用,避免对象拷贝
private:
//其余部分都不变
};
测试程序:
//测试程序
int main(){
StrBlob b1;
{//新作用域
StrBlob b2 = {"first element","second element"};
cout<<"before assignment : "
<<b2.look_data().use_count()<<endl;
b1 = b2;
cout<<"after assignment : "
<<b2.look_data().use_count()<<endl;
}//b2被销毁,计数器递减
//b1仍指向b2的原数据
cout<<b1.front()<<endl;
//打印此时b1的计数器
cout<<"b2 has been dstoryed : "
<<b1.look_data().use_count()<<endl;
return 0;
}
输出结果:
如果look_data用值返回,而不是引用返回,那么会存在拷贝【见6.2.2节笔记】,所有计数器的值会+1
三、直接管理内存
使用new分配内存
- new分配动态内存
- delete销毁动态内存
new和delete与智能指针不同,类对象的拷贝、赋值和销毁操作都不会默认地对动态分配的对象进行管理,无论是对象的创建还是销毁,都需要程序员显式地操作,在大型的应用场景中会十分复杂。
在熟悉C++拷贝控制之前,尽量只使用智能指针,而不是本节的方法管理动态内存
使用new动态分配和初始化对象
new type_name:返回一个指向该对象的指针
//pi指向一个动态分配,默认初始化的无名对象
int *pi = new int;
/)
{
connection c = connect(&d);//打开链接
unique_ptr<connection, decltype(end_connection)*>
p(&c, end_connection);
//使用链接
//当f退出时(即使是由于异常而退出)
//connection会调用end_connection正常退出
}
注意decltype(end_connection)
返回一个函数类型,而函数类型不能作为参数,函数指针可以
所以要加上*表示函数指针p(&c, end_connection)
中,类似于数组名表示指针一样,函数名实际上就表示函数指针
所以也可写作p(&c, &end_connection)
,但没必要。【前一个&表示引用传递,后一个&表示取址得到指针】
总结
到此这篇关于c++动态内存管理与智能指针的文章就介绍到这了,更多相关c++动态内存管理与智能指针内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!