12 动态内存
目录
- 0. 预备知识:程序的内存分配
- 1. 动态内存与智能指针
- 1.1 shared类:允许多个指针指向同一对象
- 1.2 直接管理内存:不用智能指针,只借助C++提供的new和delete两个运算符
- 1.3 shared_ptr和new结合使用:用new返回的指针来直接初始化智能指针,但最好的方法还是用make_shared
- 1.4 总结智能指针shared_ptr的初始化方式
- 1.5 智能指针和异常
- 1.6 智能指针和哑类:shared_ptr也可以管理其他类型的资源而不只是动态内存,须定义自己的删除器函数来替代shared_ptr默认的delete
- 1.7 智能指针陷阱(智能指针使用的最佳建议)
- 1.8 unique_ptr:某一个时刻只能有一个unique_ptr指向一个给定的对象。
- 1.9 weak_ptr:一种不控制所指向对象生存期的智能指针
- 2. 动态数组
0. 预备知识:程序的内存分配
- 堆和栈的申请方式
- stack:由系统自动分配。例如,声明在函数中一个局部变量:int b; //系统自动在栈中为b开辟空间
- heap: 需要程序员自己申请,并指明大小。
- 在c中malloc函数如:p1= (char *)malloc(10);
- 在C++中用new运算符如:p2 = new char[10]; 但是注意p1、p2本身是在栈中的。
- 堆和栈的区别可以用如下的比喻来看出:
- 使用栈:就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
- 使用堆:就像是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
1. 动态内存与智能指针
- 动态内存管理:在C++中通过一对运算符来完成
- new:在动态内存中为对象分配空间并返回一个指向该对象的指针。我们可以选择对对象进行初始化;
- delete:接受一个指向动态对象的指针,销毁该对象,并释放与之关联的内存。
- 动态内存的使用容易出现内存泄漏,引用非法内存的指针等错误:
- 有时我们会忘记释放内存,在这种情况下就会产生内存泄漏;
- 有时在尚有指针引用内存的情况下我们就释放它了,在这种情况下就会产生引用非法内存的指针(非法指针)。
- 为了更容易(同时也安全)地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象,定义在头文件memory中
- 智能指针是c++ 中管理资源的一种方式,用智能指针管理资源,不必担心资源泄露,将c++ 程序员 从指针和内存管理中解脱出来
- 智能指针:
- 管理动态对象
- 行为类似常规指针。
- 负责自动释放所指向的对象
- 新标准库提供的这两种智能指针的区别在于管理底层指针的方式:
- shared_ptr:允许多个指针指向同一对象;
- unique_ptr:“独占”所指向的对象。
- 标准库还定义了一个名为weak_ptr的伴随类:一种弱引用,指向shared_ptr所管理的对象。
- shared_ptr和unique_ptr都支持的操作
- 使用智能指针来管理动态内存,只需将智能指针指向动态内存,之后销毁啥的都不用你操心了,智能指针自动帮我们搞定
1.1 shared类:允许多个指针指向同一对象
- shared_ptr独有操作
- 类似vector,智能指针也是模板。因此,当我们创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。与vector一样,我们在尖括号内给出类型:
shared_ptr<string> p1 ; //p1是空智能指针
shared_ptr<list<int> > p2; //shared_ptr,可以指向int的list
- 智能指针的使用方式与普通指针类似。解引用一个智能指针返回它所指的对象。如果在一个判断条件中使用智能指针,效果就是检查它是否为空
- 默认初始化的智能指针中保存着一个空指针(p.get() = nullptr)。智能指针可作为条件判断其是否为空。
// 示例1
shared_ptr<int> p;
if (p) // 若不是空指针,则对指针解引用
cout << "p is not nullptr : " << *p << endl;
else // 若是空指针,则输出提示
cout << "p is nullptr" << endl;
// 示例2
// 如果p1不为空,检查它是否指向一个空string
if(p1&&p1->empty())
*p1="hi";
make_shared函数:最安全的分配和使用动态内存的方法
- make_shared函数:创建一个动态内存的对象并返回指向它的shared_ptr。将动态内存和它的智能指针联系起来, 是最安全的分配和使用动态内存的方法
- 最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向对象的shared_ptr。
- 与智能指针一样,make_shared也定义在头文件memory中。
- 当要使用make_shared时,必须指定想要创建的对象的类型。定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型:
//指向一个值为42的int的shared_ptr
shared_ptr<int> p3=make_shared<int> (42);
cout << *p3 << endl; //42
//p4指向一个值为"99999"的string
shared_ptr<string> p4=make_shared<string> (5,'9');
cout << *p4 << endl;//99999
//p5指向一个值初始化的int,即,值为0
shared_ptr<int> p5=make_shared<int> ();
cout << *p5 << endl;//0
// 这样定义看着长,我们可以用神器auto:
auto p3 = make_shared<int>(42);
auto p4 = make_shared<string>(3, '9');
auto p5 = make_shared<int>();
//p6指向一个动态分配的空vector<string>
auto p6=make_shared<vector<string>> ();
动态内存对象引用次数递增递减情况
- 我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,它所关联的计数器都会递增。
- 递增计数器:拷贝一个shared_ptr(如用make_shared等初始化一个shared_ptr、用shared_ptr赋值另一个shared_ptr、shared_ptr作为参数传入函数、函数返回shared_ptr)时,引用计数+1;
- 递减计数器:shared_ptr被赋值,或shared_ptr被销毁(如一个局部的shared_ptr离开作用域)时,引用计数-1.
auto r=make_shared<int>(42); //r指向的int只有一个引用者
r=q; //(1)给r赋值,令它指向另一个地址(2)递增q指向的对象的引用计数(3)递减r原来指向的对象的引用计数(4)r原来指向的对象已没有引用者,会自动释放
- 示例
#include <iostream>
#include <memory>
using namespace std;
int main(void){
auto p = make_shared<int>(42);//p指向的对象此时只有p一个引用者
auto q(p);//p和q指向相同的对象,此对象有两个引用者
cout << p.use_count() << endl;//2
cout << q.use_count() << endl;//2
q = make_shared<int>(1024);//给q开一个新的内存,q此时不再和p一起指向存储42那块内存
cout << p.use_count() << endl;//1
cout << q.use_count() << endl;//1
p = q;//给p赋值,此时p和q同时指向存储1024的这块内存
cout << q.use_count() << endl;//2 递增q指向的对象的引用计数
//递减原来指向的对象的引用计数,p原来指向的对象引用计数变为0,自动释放该对象的内存(存储42的这块内存)
return 0;
}
- 构造函数控制初始化,析构函数控制销毁操作,一般用来释放对象所分配的资源。
- shared_ptr会自动销毁所管理的对象(通过析构函数销毁智能指针),还会自动释放相关联的内存
- 当shared_ptr被析构函数销毁时,shared_ptr的析构函数会递减它所指对象的引用计数(shared_ptr所指对象的引用计数-1)。如果引用计数变为0,shared_ptr的析构函数便会销毁该对象,并释放对象所占用的内存;如果不为0,所指对象并不会销毁
shared_ptr<Foo> factory(T arg)
{
return make_shared<Foo>(arg);// 返回shared_ptr,指向一个类型为Foo的对象,用类型为T的arg初始化
}
void use_factory(T arg)
{
shared_ptr<Foo> p = factory(arg);// 调用factory初始化p,p所指对象的引用计数+1变成1
}// use_factory执行结束,销毁p,p所指对象的引用计数-1变成0,销毁对象并释放内存
shared_ptr<Foo> use_factory(T arg)
{
shared_ptr<Foo> p = factory(arg);// 调用factory初始化p,p所指对象的引用计数+1变成1
return p;// 返回一个p的拷贝,p所指对象的引用计数+1变成2
}// use_factory执行结束,p离开了作用域被销毁,p所指对象的引用计数-1变成1,不会销毁对象和释放内存
// 对于一块内存,shared_ptr类保证只要有任何shared_ptr对象引用它,它就不会被释放掉。
// 由于在最后一个shared_ptr销毁前内存都不会释放,保证shared_ptr在无用之后不再保留就非常重要了。
- 如果你忘记了销毁程序不再需要的shared_ptr,程序仍会正确执行,但会浪费内存。shared_ptr在无用之后仍然保留的一种可能情况是,你将shared_ptr存放在一个容器中,随后重排了容器,从而不再需要某种元素。在这种情况西下,你应该确保erase删除那些不再需要的shared_ptr元素。
- 注意:如果你将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。
程序使用动态内存的三种原因:
- (1)程序不知道自己需要使用多少个对象(比如容器类)
- (2)程序不知道所需的准确类型(如第15章面向对象程序设计)
- (3)程序需要在多个对象间共享数据(如自定义类)
- 类中定义一个share_ptr型private数据成员,在默认构造函数中初始化该数据成员。该数据成员就可以在多个对象间作为共享数据
- 使用动态内存的一个常见原因是允许多个对象共享相同的状态
- 如对象的不同拷贝都引用内存中同一个地方,一个拷贝销毁时该内存不会被销毁,因为这个内存还被另一个对象引用
/*
我们之前用过的类中,分配的资源都与对应对象生命周 期一致。如:每个 vector “拥有” 其自己的元素。
当我们拷贝一个 vector 时,原 vector 和 副本 vector 中的元素是相互分离的:
*/
vector<string> v1;//空vector
{// 新作用域
vector<string> v2 = {"f", "jf"};
v1 = v2;//从v2拷贝元素到v1中
} // 离开作用域,v2被销毁,其中元素也被销毁
// 但是v1还是有元素的,是原来v2中元素的拷贝,v1和v2的元素独立,不受v2销毁的影响
// 注意:由一个 vector 分配的元素只有当这个 vector 存在时才存在。
// 当一个 vector 被销毁时,其中的元素也都被销毁
/*
但某些类分配的资源具有与原对象相独立的生存期。
如,假定我们希望定义一个名为 Blod 的类,保存一组元素。
与容器不同,我们希望 Blod 对象的不同拷贝之间共享相同的元素。
即,希望在拷贝的时候,不去拷贝元素,而是引用内存中同一块地方。
如果两个对象共享底层数据,则当某个对象被销毁时,它的元素不能单方面销毁。
即,当我们拷贝一个 Blod 时,原 Blod 对象及其拷贝应该引用相同的底层元素。
*/
// 通常,如果两个对象共享底层数据,当某个对象被销毁时,我们不能单方面销毁底层数据:
Blod<string> b1;//空Blod
{// 新作用域
Blod<string> b2 = {"f", "jf"};
v1 = v2;//从b2拷贝元素到b1中
}// 离开作用域,b2被销毁,但其中元素不能销毁
// b1指向最初由b2创建的元素
// 注意:b1 和 b2 共享相同的元素,当 b2 离开作用域时,这些元素必须保留,因为 b1 仍在使用它们
- 例子:定义一个管理string的Blod 类并命名为 strBlod——使用动态内存
- 实现一个新的集合类型最简单的方法就是使用某个标准库容器来管理元素,采用这种方法,我们可以借助标准库类型来管理元素所使用的内存空间。这会省很多事,在这里,我们借助vector来保存元素。
- 我们当然不能直接用vector,我们要把vector保存在动态内存中,这样它的生存期就完全由我们(借助智能指针)来掌控。
- 如不能在一个Blob对象内直接保存vector,因为一个对象的成员在对象销毁时也会被销毁。如假定b1和b2是两个Blob对象,共享相同的vector。为了保证b2被销毁时vector中的元素继续存在,我们将vector保存在动态内存中
- 策略:为了实现我们希望的数据共享,我们为每个StrBlob设置一个shared_ptr(即下面的data成员)来管理动态分配的vector,这个shared_ptr的成员会记录有多少个StrBlob共享相同的vector,并在vector的最后一个使用者被销毁时释放vector
/*
为了简化问题的复杂度,我们的StrBlob只会实现vector的一小部分操作,下面代码是我们自定义一个类:
我们的类有一个默认构造函数和一个构造函数,接受单一的initializer_list<string>类型参数。此构造函数可以接受一个初始化器的花括号列表。
*/
#include <iostream>
#include <string>
#include <memory> //智能指针和动态分配内存
#include <vector>
#include <initializer_list> //初始值列表
using namespace std;
class strBlod{
public:
// 类型别名
typedef vector<string>::size_type size_type;
// 构造函数
strBlod(); // 这只是个函数声明
// 初始化列表构造函数
strBlod(initializer_list<string> il); //就是列表初始化的意思{"a", "b"}
~strBlod();
// 其他成员函数
// 这里的const是为了让const对象也可以访问
size_type size() const {
return data->size();
}
bool empty() const {
return data->empty();
}
// 添加和删除元素
void push_back(const std::string &t){
data->push_back(t);
}
void pop_back();
// 元素访问
string& front();
string& front() const;
string& back();
string& back() const;
int get_use_count() const{ //得到shared_ptr成员data的引用计数
return data.use_count();
}
private:
shared_ptr<vector<string>> data; // data是一个指向vector的智能指针
// 检查函数,当我们访问容器时检查下标是否越界:如果data[i]不合法,抛出一个异常
void check(size_type i, const string &msg) const;
string& front_display() const;
string& back_display() const;
};
// 两个构造函数都使用初始化列表来初始化data成员,让它指向一个刚创建的动态分配的vector,默认构造函数分配的是空vector:
strBlod::strBlod(): data(make_shared<vector<string>>()) {} //默认构造函数:创建动态内存对象
strBlod::strBlod(initializer_list<string> il) : data(make_shared<vector<string>>(il)) {}
strBlod::~strBlod() {}
void strBlod::check(size_type i, const string &msg) const {
if(i >= data->size()) // 不在范围内
throw out_of_range(msg);
}
// 重载函数:const和non_const是因为有时候有const StrBlob对象,此时需要用const函数
// 调用const版本时对象是const,所以this指针也是const,通过转换this指针才能调用const版本
// const版本的,当const StrBlob A时,数据成员也是const的
string& strBlod::front(){
return front_display();
}
string& strBlod::front() const{
return front_display();
}
string& strBlod::front_display() const{
//如果vector为空,check会抛出一个异常
check(0, "front on empty strBlod!");
return data->front();
}
string& strBlod::back(){
return back_display();
}
string& strBlod::back() const{
return back_display();
}
string& strBlod::back_display() const{
check(0, "back on empty strBlod!");
return data->back();
}
void strBlod::pop_back(){
check(0, "pop_back on empty strBlod!");
data->pop_back();
}
int main(void){
strBlod b1; // b1执行默认构造
cout << b1.get_use_count() << endl;//1
{
strBlod b2({"hello", "word", "!"});
cout << b2.get_use_count() << endl;//1
b1 = b2; // b1和b2共享元素
cout << b1.get_use_count() << endl;//2
cout << b1.get_use_count() << endl;//2
// 注意:当我们拷贝、赋值或销毁一个 strBlod 对象时,它的 shared_ptr 成员会被拷贝、赋值或销毁
}
// string s = b2.front();//错误,离开了b2的作用域,b2被销毁了
//b1指向原本由b2创建的元素
cout << b1.get_use_count() << endl; // 1
string s = b1.front();
cout << s << endl; // hello
return 0;
}//离开b1的作用域,b1被销毁
// 由strBlod构造函数分配的vector已经没有strBlod对象指向它,此时被自动销毁
// 如果一个 shared_ptr 的引用计数变为 0,它所指像的对象会被自动销毁。
// 因此,对于由 strBlod 构造函数分配的 vector,当最后一个指向它的 strBlod 对象被销毁时,它也会随之被自动销毁
1.2 直接管理内存:不用智能指针,只借助C++提供的new和delete两个运算符
- 直接管理内存:不用智能指针,自力更生,只借助C++提供的两个运算符来直接管理动态内存,会发现自己管理真是又烦又容易出错
- C++语言定义了两个运算符来分配和释放动态内存:
- new分配动态内存
- delete释放new分配的内存
- new和delete的两部分操作:内存分配(释放)和 对象构造(析构)
- 相对于智能指针,使用这两个运算符管理内存非常容易出错。而且,自己直接管理内存的类与使用智能指针的类不同,它们不能依赖类对象拷贝、赋值和销毁操作的任何默认定义。因此,使用智能指针的程序更容易编写和调试。
- 若类中使用智能指针,则类可以使用默认版本的拷贝、赋值和销毁成员函数,而new和delete不行。故应尽量使用智能指针,而避免使用new和delete
new分配动态内存
- 使用new动态分配和初始化对象:new返回指向动态内存的普通指针
- 在自由空间分配的内存是无名的。因此,new无法为其分配的对象命名,而是返回一个执行对象的指针:
int a; //在栈内保存,内存变量名为a,值初始化为0
int *pi=new int ;// pi指向一个动态分配的、未初始化的无名对象
// 在堆保存,而且用new分配的内存是无名的,所以只能用指针指向它
// 此new表达式在自由空间构造一个int型对象,并返回指向该对象的指针。
- 默认情况下,动态分配的对象是默认初始化的:
- 这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认函数进行初始化:
string *ps=new string; //类类型对象会调用默认构造函数初始化,ps指向空string
int *pi=new int ; // pi指向一个未初始化的int(pi指向一个类型为int、未定义的无名对象)
- 也可以使用直接初始化方式:
- 可以使用传统的构造方式(使用圆括号)
- 在新标准下,也可以使用列表初始化(使用花括号)
int *pi=new int(1024) ;//pi的对象的值为1024
string *p6 = new string(3,'1');// p6指向值为"111"的对象
vector<int> *pv=new vector<int>{0,1,2,3,4,5,6,7,8,9};
vector<int> *pv=new vector<int>{0,1,2,3,4,5,6,7,8,9};
- 也可以对动态对象进行值初始化,只需类型名之后跟一对空括号:
- (1)对于定义了自己的构造函数的类类型(例如string)来说,不管是值初始化还是默认初始化,对象都会通过默认构造函数来初始化:
string *ps1=new string; //默认初始化为空string
string *ps=new string(); //值初始化为空string
int *p3 = new int(); // p3指向值为0的对象
- (2)对内置类型来说,值初始化和默认初始化差别大:由于它没有默认构造函数,默认初始化的对象的值则是未定义的;而值初始化的内置类型对象有着良好定义的值。所以内置类型比较适合值初始化
int *pi1=new int; //默认初始化:*pi1的值未定义
int *pi2=new int(); // 值初始化为0: *pi2为0
- (3)依赖编译器合成的默认构造函数的内置类型成员:
- 类内没有初始值的话,情况和(2)一样
- 类内有初始值,就用这个初始值
- 使用auto配合初始化:
- 如果我们提供了一个括号包围的初始化器,就可以使用auto从此初始化器来推断我们想要分配的对象的类型。
- 但是,由于编译器要用初始化器的类型来推断要分配的类型,只有当括号中仅有单一初始化器时才可以对new使用auto:
auto p1=new auto(obj) ; //p指向一个与obj类型相同的对象
auto p2=new auto(a,b,c) ; //错误:括号中只能有单个初始化器
// p1的类型是一个指针,执行从obj自动推断的类型。
// 若obj是一个int,那么p1就算int*;若obj是一个string,那么p1是一个string*;依次类推。
// 新分配的对象用obj的值进行初始化
- 动态分配的const对象:必须初始化
- 类似其他任何const对象,一个动态分配的const对象必须进行初始化。
- 对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。
用new分配const对象是合法的:
//分配并初始化一个const int
const int *pci=new const int(1024);
//分配并默认初始化一个const的空string
const string *pcs=new const string;
- 总结new分配动态内存示例
#include <iostream>
#include <vector>
using namespace std;
int main(){
int *pi = new int; //pi指向一个动态分配的,未初始化的对象
cout << *pi << endl; //默认情况下,动态内存分配的对象是默认初始化的
//即,内置类型或组合类型的对象的值将是未定义的(随机值),而类类型对象将用默认构造函数进行初始化
string *ps = new string; //初始化为空string
int *p1 = new int(1024); //构造初始化,p1指向一个值为1024的int对象
string *ps1 = new string(3, '9'); //ps1指向一个值为"999"的string对象
vector<int> *pv1 = new vector<int>{0, 1, 2, 3, 4}; //列表初始化
//值初始化(类型名后面跟一对括号)
string *ps2 = new string; //默认初始化为空string
string *ps3 = new string(); //值初始化为空string
int *p2 = new int(); //值初始化为0
cout << *p2 << endl; //0
//使用auto推断
int obj = 1024;
int obj1 = 1, obj2 = 2, obj3 = 3;
auto p4 = new auto(obj); //p4指向一个与obj类型相同的对象
// 动态分配const对象
const int *p6 = new const int(1024); //分配并初始化一个const int
// *p6 = 0; //错误,p6指向的对象是const int
const string *ps4 = new const string; //分配并默认初始化一个const的空string
return 0;
}
- 内存耗尽:若分配内存失败(如堆内存已耗尽),new返回空指针,抛出异常bad_alloc。使用定位new可传递额外参数nothrow避免抛出异常。
- bad_alloc和nothrow都定义在头文件new中。
- 虽然现代计算机通常都配备大容量内存,但是自由空间被耗尽的情况还是有可能发生。
- 一旦一个程序用光了它所有可用的内存,new表达式就会失败。
- 默认情况下,如果new不能分配所要求的内存空间,它就会抛出一个类型为bad_alloc的异常。
- 我们可以改变使用new的方式来阻止它抛出异常,使得程序进行运行。
- 我们称这种形式的new为定位new。定位new表达式允许我们向new传递额外的参数,在下例中,我们传递给它一个由标准库定义的名为nothrow的对象。如果将nothrow传递给new,我们的意图是告诉它不能抛出异常。如果这种形式的new不能分配所需内存,它会返回一个空指针。
//如果分配失败,new返回一个空指针
int *p1=new int ; //如果分配失败,new返回空指针,抛出std::bad_alloc异常
int *p2=new (nothrow) int ; //如果分配失败,new返回一个空指针
//定位new,传递nothrow
delete释放动态内存
- 为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统。
- 我们通过delete表达式来将动态内存归还给系统。delete表达式接受一个指针,指向我们想要释放的对象,即:
- 形式:delete 指向动态内存的指针
- 示例:delete p; // p必须指向一个动态分配的对象或是一个空指针
- delete表达式执行两个动作:销毁给定的指针所指的对象;释放对应的内存。
- 我们*传递给delete的指针必须指向动态分配的内存,或是一个空指针(程序员自觉把控)
- 释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为都是未定义的。
- 编译器通常无法分辨指针指向的是静态内存还是动态内存,也无法分辨指针所指的内存是否被释放过。
int i, *pi1=&i, *pi2=nullptr;
double *pd=new double(33),*pd2=pd;
delete i; //错误,i不是一个指针
delete pi1; //未定义:pi1指向一个局部变量
delete pd; //正确
delete pd2; //未定义,pd2指向的内存已经被释放了
delete pi2; //正确:释放一个空指针总是没有错误的
/*
对于delete i的请求,编译器会生成一个错误信息,因为它知道i不是一个指针。
执行delete pi1和pd2所产生的错误则更具潜在危害:
通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。
类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放了
对于这些delete表达式,大多数编译器会编译通过,尽管它们是错误的。
*/
- 一个const对象的值不能被改变,但它本身是可以被销毁的。如同任何其他动态对象一样,想要释放一个const动态
const int *pci=new const int(1024);
delete pci; //正确:释放一个从const对象
- 动态对象的生存期直到被释放时为止
- 返回指向动态内存的普通指针(而不是智能指针)的函数给其调用者增加了一个额外负担:调用者必须记得释放内存。
- 普通指针被销毁,它指向的动态内存并不会被自动释放。
- 由shared_ptr管理的内存在最后一个shared_ptr销毁时会被自动释放。
- 由内置指针(而不是智能指针)管理的动态内存(动态对象)在被显式释放之前一直都会存在。
// factory返回一个指针,指向一个动态分配的对象
Foo* factory(T arg)
{
// 视情况处理arg
return new Foo(arg); //调用者负责释放此内存
// 该factory函数分配一个对象,但并不delete它。factory的调用者负责在不需要此对象时释放它。不幸的是,调用者经常忘记释放对象
}
void use_factory(T arg)
{
Foo *p=factory(arg);
// 使用p但不delete它
} // 局部变量p是一个内置指针而不是智能指针,在这里P离开了它的作用域被销毁,但它所指向的内存没有被释放
// p是指向factory分配的内存的唯一指针。一旦use_factory返回,p被销毁,程序就没有办法释放这块内存了
// 修正这个错误的正确方法是在use_factory中记得要释放内存:
void use_factory(T arg)
{
Foo *p=factory(arg);
//使用p
delete p ; //现在记得释放内存,我们已经不需要它了
}
// 还有一种可能,我们的系统中的其他代码要使用use_factory所分配的对象,
// 我们就应该修改此函数,让他返回一个指针,指向它分配的内存:
Foo* use_factory(T arg)
{
Foo *p=factory(arg);
//使用p
return p; //调用者必须释放内存
}
- 与类类型不同,内置类型的对象被销毁时什么也不会发生。特别是,当一个指针离开其作用域时,它所指的对象什么也不会发生。如果这个指针指向的是动态内存,那么内存将不会被自动释放。
使用new和delete管理动态内存存在的三个常见问题
- (1)忘记delete内存
- 忘记释放动态内存会导致人们常说的“内存泄漏”问题,因为这种内存不可能被归还给自由空间了。
- 查找内存泄漏错误是非常困难的,因为通常应用程序运行很长时间后,真正耗尽内存时,才能检测到这种错误。
- (2)使用已经释放掉的对象
- 通过在释放内存后将指针置为空,有时可以检测出这种错误
- (3)同一块内存被释放两次
- 当有两个指针指向相同的动态分配对象时,可能发生这种错误。
- 如果对其中一个指针进行了delete操作,对象的内存就被归还给自由空间了,如果我们随后又delete第一个指针,自由空间就可能会被破坏。
- 坚持只使用智能指针,就可以避免所有这些问题。对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它。
空悬指针:指向已被释放的内存的指针。
- 空悬指针:即指向一块曾经保存数据对象但现在已经无效的内存的指针。
- 避免空悬指针的方法:
- 避免过早delete p,在p即将离开其作用域之前再delete p。
- 若需要保留指针,可在delete p后进行空指针赋值:p=nullptr,该方法仅对p有效,对其它指向相同内存的指针无效。
- 如果有多个普通指针指向相同的内存,在delete内存之后重置指针的方法只对这个指针有效,对其他任何指向(已释放的)内存的指针是没有作用的。
int *p(new int (42)); //p指向动态内存
auto q=p; //p和q指向相同的内存
delete p; //p和q均变为无效,该内存被释放
p=nullptr; //指出p不再绑定到任何对象,解决了空悬指针的问题
/*
本例中p和q指向相同的动态分配的对象。我们delete此内存,然后将p置为nullptr指出它不再指向任何对象,
但是,重置p对q没有任何作用,在我们释放p所指向的(同时也是q所指向的)内存时,q也变为无效了,它变成了一个空悬指针。
*/
1.3 shared_ptr和new结合使用:用new返回的指针来直接初始化智能指针,但最好的方法还是用make_shared
- 定义和改变shared_ptr的其他方法:
- 可以用new返回的指针来初始化智能指针。
- 接受指针参数的智能指针构造函数是explicit的(explicit表示禁止隐式转换)。因此,我们不能将一个内置指针隐式转化为一个智能指针,必须使用直接初始化形式来初始化为一个智能指针:
shared_ptr<int> p1(new int(24)); //正确,直接初始化形式,p1指向一个值为42的int
shared_ptr<int> p2 = new int(24); //错误,用new构造临时对象时返回的是一个普通指针int*,它试图隐式转换为智能指针。
- 由于我们不能进行内置指针到智能指针的隐式转化,一个返回shared_ptr的函数不能在其返回语句中隐式转化一个普通的指针:
shared_ptr clone(int p){
return new int(p); //错误:试图隐式转换为shared_ptr
}
//正确示例
//我们必须将shared_ptr显式地绑定到一个想要返回的指针上
shared_ptr clo(int p){
return shared_ptr<int>( new int(p) ); // 使用shared_ptr直接转换,类似int(…)
}
- 默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存(就是说它得指向new出来的),因为智能指针默认使用delete释放它所关联的对象。
- 我们当然也可以让智能指针指向其他类型的资源,但是你得提供自己的释放操作来替代delete。
不要混合使用普通指针和智能指针
- 一旦用shared_ptr绑定到一个指向动态内存的普通指针,我们就把内存的管理责任交给这个shared_ptr。一旦这样做了!我们就不要再使用这个内置指针来访问shared_ptr所指向的内存。(因为你用这个内置指针时,内存可能已经被这个智能指针释放了)
- 使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。
// 考虑下面对shared_ptr进行操作的函数:
void process(shared_ptr<int> ptr){
//使用ptr
}//离开作用域,ptr被销毁
/*
process的参数是传值方式传递的,因此实参会被拷贝到ptr中。
拷贝一个shared_ptr会递增其引用计数,因此,在process运行过程中,引用计数值至少为2.
当process结束时,ptr的引用计数会递减,但不会变为0
。因此当局部变量ptr被销毁时,ptr指向的内存不会被释放。
*/
// 使用此函数的正确方式是 传递给它一个shared_ptr:
shared_ptr<int> p(new int(42)); //引用计数为1
process(p); //拷贝p会递增它的引用次数;在process里引用次数值为2
int i = *p; //正确:引用次数值为1
// 不能传递给process一个内置指针,但可以传递给它一个(临时的)shared_ptr
int *x(new int(1024)); //危险:x是一个普通指针,而不是一个智能指针
process(x); //编译错误:不能将int*转化为一个shared_ptr<int>
process(shared_ptr<int>(x)); //合法:将普通指针显式转换成智能指针,但出了process,x所指向的内存会被释放
int j = *x; //未定义的,x为一个空悬指针。解指针就会产生难以预料的结果
// 上面的调用中,我们将一个临时shared_ptr传递给process。
// 当这个调用表达式结束时,这个临时对象也就被销毁了。引用计数递减为0,所指向的内存会被释放。
不要使用get初始化另一个智能指针或为智能指针赋值
- 智能指针定义了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象。
- 利用get()函数得到的内置指针来初始化一个临时的智能指针,一旦调用结束,该临时智能指针被销毁,内置指针所指对象内存会被释放,使得p变为空悬指针,即p还没有销毁,但它指向的内存却已经被这个临时智能指针释放了,使用该智能指针会发生未定义的行为,而且当其被销毁时,这块内存会被第二次释放。
- get函数为下面一种情况所设计的:
- 我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete此指针。
shared_ptr<int> p(new int(42)); //引用计数为1
int *q = p.get(); //正确,但不要让q所管理的内存释放
{ //新的程序块
//未定义:两个独立的shared_ptr指向相同的内存
shared_ptr<int>(q);
} //程序块结束,q被销毁,它指向的内存被释放
int foo = *p; //未定义:P指向的内存被释放了
// 当我们使用p时会发生未定义的行为,而且当p销毁时,这块内存会被第二次delete
- get用来将指针的访问权限传递给代码。你只有在确定不会delete指针的情况下,才能使用get。特别是,永远不要get初始化另一个智能指针或者为另一个智能指针赋值。
shared_ptr的reset操作
- 利用reset()函数将一个新的指针赋予一个shared_ptr。
- 与赋值类似,reset会更新引用计数。(旧对象减1,新对象加1)
shared_ptr<int> p(new int(34));
p = new int(1024); //错误:不能将一个普通指针赋予shared_ptr(禁止隐式转换)
p.reset(new int(1024)); //正确:shared_ptr类型的p指向一个新对象
shared_ptr的unique操作:reset成员经常和unique一起使用,来控制多个shared_ptr共享的对象
- shared_ptr的unique操作: 如果一个shared_ptr对象唯一指向它所管理的内存对象,则返回true。
- 如果某个对象由多个shared_ptr共享,当要用其中一个智能指针改变该对象的值,最好将该智能指针重置,指向与旧对象相等的新对象,在新对象修改值而不影响旧对象的值(毕竟旧对象有很多人在用,你一个人改了,会影响到其他人的工作。就像python环境,大家都在用,如果你自己在这个环境升级了一个包的版本,就会影响其他人的工作;正确做法是自己独自弄个虚拟环境)
- 在改变低层对象之前,要检查自己是否是当前对象仅有的用户;如果不是(即存在多个智能指针指向某个动态内存对象),就要在改变之前做一份拷贝。
if(!p.unique())
p.reset(new string(*p)); //我们不是唯一用户;分配新的拷贝
//如果p不是它所管理对象的唯一引用者,则为它重新分配
*p += newVal; //现在p是唯一的用户,可以改变对象的值。
1.4 总结智能指针shared_ptr的初始化方式
- 不能将指向动态内存的普通指针隐式转换为智能指针
- 如果我们不初始化一个智能指针,它就会被初始化为一个空指针
shared_ptr<int> p1; // nullptr
- 构造函数初始化:shared_ptr和new结合使用
shared_ptr<int> p2(new int(1));
//shared_ptr<int> p2 = new int(2); // error
shared_ptr<int> p3 = p2;//裸指针
shared_ptr<string> p4(new string("AAA"));
shared_ptr<string> p5 = shared_ptr<string>(new string("AAA"));
//shared_ptr<string>将new返回的指针转换为指向string的智能指针
- (最推荐)make_shared函数初始化:make_shared
(args)
shared_ptr<string> p6 = make_shared<string>();
//shared_ptr<string> p6;
//p6(make_shared<string>());
//或者shared_ptr<string> p6(make_shared<string>());
shared_ptr<string> p7 = make_shared<string>("hello");
shared_ptr<string>p8 = make_shared<string>(10,'9');//传递的参数必须与string的某个构造函数相匹配
//auto关键字代替shared_ptr,p5指向一个动态分配的空vector<string>
auto p9 = make_shared<vector<string>>();
- 通过reset方法重新指定指向对象:reset来将一个新的指针赋予一个shared_ptr,旧对象引用次数减1
shared_ptr<int> pointer;
pointer.reset(new int(1));
shared_ptr<int> p(new int(34));
p = new int(1024); //错误:不能将一个普通指针赋予shared_ptr(禁止隐式转换)
p.reset(new int(1024)); //正确:shared_ptr类型的p指向一个新对象
- reset成员和unique一起使用,来控制多个shared_ptr共享的对象
if(!p.unique())
p.reset(new string(*p)); //我们不是唯一用户;分配新的拷贝
//如果p不是它所管理对象的唯一引用者,则为它重新分配
*p += newVal; //现在p是唯一的用户,可以改变对象的值。
1.5 智能指针和异常
- 如果使用智能指针:即使程序块由于异常过早结束,智能指针类也能确保在内存不需要的时候将其释放。
void f(){
shared_ptr<int> sp(new int(11));
//假设抛出了异常,而且在f中未捕获
}//函数结束后shared_ptr自动释放内存
// 为什么即使发生异常也可以释放呢?原因在:函数退出有两种可能,正常结束或者发生了异常,
// 无论哪种情况,局部对象都会被销毁。sp销毁时候引用计数递减为0,所以此块内存释放掉。
- 如果使用内置指针管理内存:若在new和delete之间发生异常,且异常未在f中捕获,则内存永远不能释放。因为在函数之外就没有指针指向这块内存,异常就无法释放它。
void f1(){
int* ip = new int(12);
// 假设delete语句前抛出了异常,而且在f中未捕获
delete ip;
}
// 函数结束后ip所指向的内存没有被释放
1.6 智能指针和哑类:shared_ptr也可以管理其他类型的资源而不只是动态内存,须定义自己的删除器函数来替代shared_ptr默认的delete
- shared_ptr默认管理动态内存,因为shared_ptr默认对所保存指针进行释放的操作是delete。
- shared_ptr假定它们指向的是动态内存,因此当一个shared_ptr被销毁时,它默认地对它管理的指针进行delete操作。
- shared_ptr也可以管理其他类型的资源。对于那些没有或不具有良好的析构函数的类。使用智能指针是一个有效防止内存泄漏的方法
- 当我们用智能指针指向其他类型的资源而不是动态内存时,必须提供自己定义的释放操作——删除器函数来替代shared_ptr的delete(delete只能释放动态内存)
- 很多c++类都定义了析构函数,负责清理对象使用的资源。但是,并不是所有的类都有这样良好的定义。特别是那些C和C++两种语言设计的类,通常都要求用户显示的释放所使用的任何资源。
- 与管理动态内存类似,我们通常可以使用类似的技术来管理不具有良好的析构函数的类。例如,我们正在使用一个C和C++都使用的网络库,使用这个库的代码可能是:
struct destination; //正在连接什么
struct connection; //使用连接所需的信息
connection connect(destination*) //打开连接
void disconnect(connection); //关闭给定的链接
void f(destination &d /*其他参数*/){
//获取一个连接;记得使用完要关闭它
connection c = connect(&d);
//使用连接
//如果我们在f退出前忘记调用disconnect,就无法关闭c了
}
// 如果connection有一个析构函数,就可以在f结束时由析构函数自动关闭连接。
// 所以我们使用shared_ptr来保证connection被正确关闭。
- 使用我们自己的释放操作代替delete
- 为了使用shared_ptr来管理一个connection,我们必须首先定义一个函数来代替delete。这个删除器(deleter)函数必须能够完成对shared_ptr中保存的指针进行释放的操作。
- 当我们创建一个shared_ptr时,可以传递一个(可选的)指向删除器函数的参数:
// 本例中,我们的删除器必须接受单个类型为connection*的参数:
void end_connection(connection* p) {disconnect(*p)};
void f(destination &d/*其他参数*/){
connection c= connect(&d);
// 当我们创建一个shared_ptr时,可以传递一个(可选的)指向删除器函数的参数
shared_ptr<connection> p(&c,end_connection);
//使用连接
//当f退出时(即使由于异常退出),connection会被正确关闭
}
/*
当p被销毁时,它不会对自己保存的指针执行delete,而是调用end_connection。然后end_connection会调用disconnect,从而确保连接被关闭。
如果f正常退出,那么p的销毁会作为结束处理的一部分。如果发生了异常,p同样会被销毁,从而连接被关闭。
*/
- 编写自己版本用shared_ptr管理connection的函数,并用lambda代替
#include <iostream>
#include <memory>
using namespace std;
struct destination {};
struct connection {};
connection connect(destination* pd) {
cout << "打开连接" << endl;
return connection();
}
void disconnect(connection c) {
cout << "关闭连接" << endl;
}
//未使用shared_ptr的版本
void f(destination& d) {
cout << "直接管理connect" << endl;
connection c = connect(&d);
//忘记调用disconnect关闭连接,将无法关闭c
cout << endl;
}
// 使用shared_ptr的版本
void end_connection(connection* p) { disconnect(*p); }
//lambda不捕获局部变量,参数为connection指针,用改指针指向的对象调用disconnect即可
//[](connection *p){disconnect(*p);}
void f1(destination& d) {
cout << "用shared_ptr管理connect" << endl;
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
//shared_ptr<connection> p(&c,[](connection *p){disconnect(*p);} );
//忘记调用disconnect关闭连接,c依旧可以正常关闭
cout << endl;
}
int main() {
destination d;
f(d);
f1(d);
return 0;
}
1.7 智能指针陷阱(智能指针使用的最佳建议)
- 不用相同的内置指针初始化(或reset)多个智能指针
- 不使用get()初始化或reset另一个智能指针。
- 不delete get()返回的指针,即不 delete p.get()
- 如果你使用get()返回的指针p.get(),记得当最后一个对应的智能指针销毁后,你的指针p.get()就无效了。
- 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。
代码来源
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class Test{
public:
Test(int d = 0) : data(d){cout << "new:" << data << endl;}
~Test(){cout << "del:" << data << endl;}
private:
int data;
};
void my_deleter(Test* t){
cout << "my_deleter is work" << endl;
}
void pro(shared_ptr<int> ptr){
}
int main(){
//test1 reset
/*
Test* tp = new Test(1);
shared_ptr<Test> stp(tp);
shared_ptr<Test> stp1(stp);
stp.reset();
cout << stp << endl;
*/
//test2 自定义删除器
/*
Test* tp = new Test(1);
//不会调用Test的析构函数了,只调用my_deleter函数
shared_ptr<Test> stp(tp, my_deleter);
shared_ptr<Test> stp1(stp);
cout << stp.use_count() << endl;
Test* tp1 = new Test(2);
stp1.reset(tp1, my_deleter);
*/
//test3 不要混用普通指针和智能指针
/*
shared_ptr<int> p(new int(42));//计数器为1
pro(p);//p作为参数会进行copy递增它的计数器,在pro内部计数器是2
int i = *p;//计数器为1
cout << i << endl;
int* bad = new int(11);
//pro(bad);//编译错误
pro(shared_ptr<int>(bad));//合法,但出了pro,bad所指向的内存会被释放
int j = *bad;//解指针bad就会产生难以预料的结果
*/
//test4 get的错误使用
/*
shared_ptr<int> p(new int(12));
int* q = p.get();
{
shared_ptr<int> tmp(q);
}//程序块结束后,q所指向的对象被释放
int f = *p;//解指针p就会产生难以预料的结果
cout << f << endl;
*/
}
1.8 unique_ptr:某一个时刻只能有一个unique_ptr指向一个给定的对象。
- unique_ptr独有操作:
- unique_ptr"拥有"其所指的对象。与shared_ptr不同,某一个时刻只能有一个unique_ptr指向一个给定的对象。当unique_ptr被销毁时,其所指对象也被销毁。
- 与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。
- 定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。unique_ptr只能使用new直接初始化:
unique_ptr<double> p1;
unique_ptr<int> p2(new int(42));
unique_ptr的release和reset操作
- 由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作:
unique_ptr<string> p1(new string("asx"));
unique_ptr<stirng> p2(p1); //错误:unique_ptr不支持拷贝
unique_ptr<string> p3;
p3 = p2; //错误:unique_ptr不支持赋值4
- 但是我们可以通过release或者reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique:
// unique_ptr的拷贝:release将所有权从p1转移给p2
unique_ptr<string> p2(p1.release()); //p2初始化:release将p1置为空,p2被初始化为release返回的p1原来保存的指针
unique_ptr<string> p3(new string("qwe"));
// unique_ptr的赋值:reset将p3所有权给p2
p2.reset(p3.release()); //p2重置:reset释放了p2原来指向的内存,重新指向p3原来保存的指针,p3置为空
- reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。如果unique_ptr不为空,它原来指向的对象被释放。
- 调用release会切断unique_ptr和它原来管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。
- 在上例中,管理内存的责任简单地从一个智能指针转移给另一个。
- 但是,如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:
p2.release(); //错误:p2不会释放内存,而我们却把指针置空,从而失去了指向这块内存的指针
auto p = p2.release(); //正确,但是我们必须记得delete(p)
- 不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要销毁的unique_ptr。
- 情形:函数传递unique参数和返回unique_ptr
// 最常见的例子是从函数返回一个unique_ptr:
unique_ptr<int> clone(int p){
//正确:从int*创建一个unique_ptr<int>
return unique_ptr<int>(new int(p)); //将普通指针显式转换为unique_ptr
}
// 还可以返回一个局部对象的拷贝:
unique_ptr<int> clone(int p){
unique_ptr<int> ret(new int(p));
...
return ret;
}
// 对于两段代码,编译器都知道返回的对象将要被销毁,此时编译器会执行一种特殊的“拷贝”。——13.6.2介绍
- unique_ptr 向后兼容 auto_ptr。auto_ptr是标准库的老版本,具有unique_ptr的部分特性。特别是,不能在容器中保存auto_ptr,也不能从函数返回auto_ptr。虽然auto_ptr仍是标准库的一部分,但编写程序时应该使用unique_ptr。
向unique_ptr传递删除器
- unique_ptr和shared_ptr都是默认使用delete释放所指对象,也支持自定义删除器来释放对象,但是它这个自定义很特殊(16.1.6介绍)。unique_ptr重载删除器时需提供删除器类型D(decltype(d) *):
//p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
//它会调用一个名为fcn的delT类型对象
unique_ptr<objT,delT> p(new objT,fcn);
// 更具体例子:重写连接程序,用unique_ptr来代替shared_ptr
void f(destination &d/*其他参数*/){
connection c= connect(&d); //打开连接
//当p被销毁时,连接将会被关闭
unique_ptr<connection,decltype(end_connection) *> p(&c,end_connection);
//其他代码
//当f退出时(即使是由于异常而退出),connection会被正确关闭
}
1.9 weak_ptr:一种不控制所指向对象生存期的智能指针
- weak_ptr操作:
- weak_ptr是一种不控制所指向对象生存期的智能指针。指向一个由shared_ptr管理的对象,但不改变shared_ptr的引用计数。
- 一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,不管有没有weak_ptr指向该对象。
- 当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); //wp弱共享;p引用计数并未改变
- 由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。
- 此函数检查weak_ptr指向的对象是否存在
- 如果存在,lock返回一个指向共享对象的shared_ptr;否则返回一个空的shared_ptr:
if(shared_ptr<int> np = wp.lock()){ //如果np不为空则条件成立
}
- weak_ptr的应用:核查指针类
- 作为weak_ptr用途的一个展示,我们将为StrBlob类定义一个伴随指针类。我们的指针类将明命名为StrBlobPtr,会保存一个weak_ptr,指向StrBlob的data成员。
- 使用weak_ptr的原因:
- 通过使用weak_ptr,不会影响一个给定的的StrBlob所指向的vector的生存期。但是,可以阻止用户访问一个不再存在的vector的企图。
- StrBlobPtr会有两个数据成员:
- wptr:指向一个StrBlob中的vector(或者为空)
- curr:保存当前对象所表示的元素的下标
// 类似它的伴随类StrBlob,我们指针类也有一个check成员来检查解引用StrBlobPtr是否安全:
//对于访问一个不存在的元素的尝试,StrBlobPtr抛出一个异常
class StrBlobPtr {
public:
StrBlobPtr():curr(0){}
StrBlobPtr(StrBlob& a,size_t sz =0):wptr(a.data),curr(sz){}
string& deref() const;
StrBlobPtr& incr();
private:
//检查下标合法性,若检查成功,check返回一个指向vector的shared_ptr
shared_ptr<vector<string>> check(size_t, const string&) const;
//保存一个weak_ptr,意味着低层vector可能会被销毁
weak_ptr<vector<string>> wptr; //指向vector的弱指针
size_t curr;//记录在数组中的当前位置
};
/*
第一个构造函数:生成一个空的STrBlobPrt。其构造函数初始化列表将curr显式初始化为0,并将wptr隐式初始化为一个空weak_ptr
第二个构造函数:接受一个StrBlob引用和一个可选的索引值。此构造函数初始化wptr,令其指向给定strBlob对象的shared_ptr中的vector,并将curr初始化为sz的值
*/
// StrBlobPtr的check()不仅要检查下标的合法性,还要负责检查指针指向的vector是否还存在
shared_ptr<vector<string>> StrBlobPtr::check(size_t i, const string& msg) const {
auto ret = wptr.lock();
if(!ret)
throw runtime_error("unbound StrBlobPtr");
if (i >= ret->size())
throw out_of_range(msg);
return ret; //成功的话返回指向vector的shared_ptr
}
// 指针操作
// 定义名为deref和incr的函数,分别用来解引用和递增StrBlobPtr
// deref()调用check(),检查vector是否以及curr是否在合法范围内:
string& StrBlobPtr::deref() const{
auto p = check(curr,"derefernce past end");
return (*p)[curr];
}
// incr()也调用check()
//前缀递增:返回递增后对象的引用
StrBlobPtr& StrBlobPtr::incr(){
check(curr,"increment past end of StrBlobPtr");
++curr;
return *this;
}
// 为了访问data成员,把指针类StrBlobPtr声明为StrBlob的friend,还要为StrBlob类定义为begin和end操作,返回一个指向它自身的StrBlobPtr:
class StrBlobPtr;
class StrBlob{
friend class StrBlobPtr;
//返回指向首元素和尾后元素的StrBlobPtr
StrBlobPtr begin(){return StrBlobPtr(*this);}
StrBlobPtr end(){
auto ret = StrBlobPtr(*this,data->size());
return ret;
}
}
// 总代码
#include <iostream>
#include <string>
#include <memory>
#incude <vector>
class StrBlobPtr
{
public:
StrBlobPtr():curr(0) {}
StrBlobPtr( StrBlob &a, size_t sz = 0):wptr(a.data),curr(sz) {}
std::string& deref() const;
StrBlobPtr& incr(); //
private:
//
std::shared_ptr<std::vector<std::string>> check(std::size_t, const std::string&) const;
std::weak_ptr< std::vector<std::string>> wptr;
std::size_t curr;
};
std::shared_ptr<std::vector<std::string>> StrBlobPtr::check(std::size_t i, const std::string& msg) const
{
auto ret = wptr.lock();
if( !ret)
throw std::runtime_error("unbound StrBlobPtr");
if( i >= ret->size())
throw std::out_of_range(msg);
return ret;
}
std::string& StrBlobPtr::deref() const
{
auto p = check(curr, "deference past end ");
return (*p)[curr];
}
StrBlobPtr& StrBlobPtr::incr()
{
check(curr, "increment past end of StrBlobPtr");
++curr;
return *this;
}
2. 动态数组
- new和delete运算符一次分配/释放一个对象,但是某些应用需要一次为很多对象分配内存的功能。例如string和vector都是在连续内存中保存它们的元素。当容器要重新分配内存时,必须要一次性为很多元素分配内存,由此需要使用动态数组。
- c++语言和标准库提供了两种一次分配一个对象数组的方法:new和allocator(推荐)
- new:** 另一种new表达式语法**,可以分配并且初始化一个对象数组
- allocator:标准库包含一个名为allocator的类,允许我们将分配和初始化分离。使用allocator通常会提供更好的性能和更灵活的内存管理能力。
- 大多数应用都没有直接访问动态数组的需求,当一个应用需要可变数量的动态对象时,通常使用vector等标准库容器(我们之前构造的动态内存的那个类StrBlob,使用了vector容器作为底层,这是一种更简单、快速、安全的方式)。
- Best Practices:大多数应用应该使用标准库容器而不是动态数组,使用容器更不容易出现内存管理错误并且可能有更好的性能。
- 使用容器的类可以使用默认版本的拷贝、赋值和析构操作,而分配动态数组的类必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存。
- 直到学习完第13章,不要在类内的代码中分配动态内存
2.1 new和数组:new 类型名[数量]
- 数组定义时是int a[]
- 为了让new分配一个对象数组,我们要在类型名之后跟一堆方括号,在其中指明要分配的对象的数目。new分配要求数量的对象并且返回指向第一个对象的指针。
- new一个动态数组:
- (1)new 类型名[数量]:类型名之后加一对方括号,在其中指明分配的对象数目(必须是整型,不必是常量)。new分配要求数量的对象返回指向第一个对象的指针。
//调用get_size确定分配多少个int
int *pia = new int[get_size()];//pia指向第一个int对象
// 方括号中大小必须是整形,但是不必要是常量。和定义静态数组不同!静态数组的定义大小必须是常量!
int a[5];//正确!
int a[N];//错误!
- (2)也可以用一个表示数组类型的类型别名来分配一个数组,这样new表达式中就不需要方括号了:
typedef int arrT[42];//arrT 表示42个int的数组类型
int *p = new arrT;//分配一个42个int的数组;p指向第一个int
// 编译器编译时:int *p = new int[42];
- 动态数组:new T[]分配的内存
- 当用new分配一个数组时,返回一个数组元素类型的指针,而非一个数组类型的对象
- 动态数组不是数组类型,故不能对动态数组调用begin或end,也不能使用范围for。
- WARNING:要记住我们所说的动态数组并不是数组类型,而是一个指向数组元素的指针。
初始化动态数组
- 初始化动态分配对象的数组:
// (1)默认情况下,new分配的对象不管是单个分配的还是数组中的,都是默认初始化。
// (2)可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号。
int *p1 = new int[3]; //默认初始化,内置类型的局部变量不初始化
int *p2 = new int[3](); //值初始化,3个值初始化为0的int
int *p3 = new int[3](1); //error
int *p4 = new auto[3](1); //error
string *psa = new string[10]; //默认初始化,string类默认初始化为空string
string *psa2 = new string[10](); //值初始化,空string
// (3)还可以使用花括号列表初始化
int *pia3 = new int[10]{0,1,2,3,...};
int *p5 = new int[3]{1,2,3}; //列表初始化
auto p6 = new auto(1); // error
string *psa3 = new string[10]{"a","an"};//前两初始化器初始化了,后面的进行值初始化。
- 如果初始化器数目大于元素数目,则new表达式失败,不会分配任何内存。new会抛出一个类型为bad_array_new_length的异常。类似bad_alloc,此类型定义在头文件new之中。
- 动态分配一个空数组是合法的,但是不能解引用:不能创建大小为0的静态数组,但可以new T[0],其返回的指针类似于尾后指针。但是此指针不能解引用,毕竟它不指向任何元素。
// 虽然我们不能创建一个大小为0的静态数组对象,但是当n == 0时候,调用new[n]是合法的:
char arr[0]; //错误:不能定义长度为0的数组
char *cp =new char[0]; //正确,但是cp不能解引用
- 释放动态数组(即delete一个动态数组):delete []p
- 为了释放动态数组(一个指向数组的指针)时,我们使用一种特殊形式delete,即在指针前加上一个空方括号对:delete []p
- 数组中的元素按逆序销毁,最后一个元素首先被销毁,然后是倒数第二个。
- WARNING:delete一个指向动态数组的指针时忽略[],或者delete一个指向单一对象的指针时添加[],两者的行为都是未定义的。
delete p; //p必须指向一个动态分配的对象或为空
delete []pa; //pa必须指向一个动态分配数组或者未空
智能指针unique_ptr和动态数组
- 指向数组的unique_ptr操作:
- 标准库提供了一个可以管理new分配的数组的unique_ptr。为了用一个unique_ptr管理动态数组,必须在对象类型后面跟一对空方括号:
···C++
//up指向一个包含10个初始化int的数组
unique_ptr<int[]> up(new int[10]);
up.release();//自动用delete[]销毁其指针
- 类型说明符中方括号(**<int[ ]>**)指出up指向一个int数组而不是一个int。由**于up指向一个数组,当up销毁它管理的指针时,会自动使用delete[]。**
- **指向数组的unique_ptr不支持点和箭头成员访问运算符;但可以使用下标运算符来访问数组中的元素:**
for(size_t i =0;i!=10;i++)
up[i] = i;
####智能指针shared_ptr和动态数组
- 与unique_ptr不用,shared_ptr不直接支持管理动态数组。**如果希望使用shared_ptr管理一个动态数组,必须提供自定义的删除器:**
```C++
shared_ptr<int> sp(new int[10], [](int *p){delete[] p});
sp.reset();//使用我们提供的lambda作为删除器释放数组,它使用delete[]
//(疑惑:为什么是shared_ptr<int>,而不是shared_ptr<int[]>)
- **如果未提供删除器,上面这段代码是未定义的。
- 因为默认情况下,shared_ptr使用delete销毁它所指向的对象。
- 如果此对象是一个动态数组,对其使用delete所产生的问题与释放一个动态数组指针时忘记[]产生的问题一样。
- shared_ptr不支持动态数组管理这一特性会影响我们如何访问数组中的元素,shared_ptr未定义下标运算符,而且智能指针类型不支持指针算术运算。为了访问数组中的元素,必须用get或缺一个内置指针,然后用它来访问数组元素。
for(size_t i =0;i!=10;++i)
*(sp.get()+i) = i; //使用get获取一个内置指针
2.2 allocator类
- new和delete的两部分操作:内存分配(释放)和 对象构造(析构)
- new将内存分配与对象构造相组合,delete将内存释放与对象析构相组合。allocator类定义在头文件memory中,可将内存分配与对象构造相分离
- 当分配一大块内存时,我们计划在这块内存上按需构造对象,在此情况下希望内存分配和对象构造分离。
- 这表示:我们可以分配大块内存,但是只在真正需要时才真正执行对象创建操作(同时付出一定开销)
- 一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费:
//动态分配的string数组
string *const p = new string[n];
string s = "aaa";
*p =s;
// new表达式分配并且初始化了n个string,但是实际我们可能并不会用到n个string,造成了浪费。
// 对于要使用的对象,在初始化之后立即赋予了新值,每个元素被赋值两次:一次在默认初始化,之后是在赋值。
- 更重要的是,没有默认构造函数的类就不能动态分配数组
- allocator类
- 头文件:memory
- 作用:帮助我们将内存分配和对象构造分离开。
- 类似于vector,allocator是一个模板。必须指明allocator可以分配的对象类型。
- allocator操作
- allocator提供一种类型感知的内存分配方法,其分配的内存是原始的,未构造的。使用未构造对象的内存,其行为是未定义的。只能对已构造对象进行destroy操作。
allocator<int> a;
size_t n = 3;
auto p1 = a.allocate(n); // 分配原始内存
auto p2 = p1;
for (size_t i = 0; i < n; ++i, ++p2) // 构造对象
a.construct(p2, i);
while (p2 != p1)
a.destroy(--p2); // 析构对象
a.deallocate(p1, n); // 释放内存
- (1)allocate():allacator对象分配一段未构造内存,保存n个类型
- 先定义一个可为T类型对象分配未构造内存的allocator对象,然后使用allocator对象的allocate()操作来分配一段可以保存n个T类型对象的原始的、未构造的内存:
allocator<string> alloc; //可以分配string的allocator对象
auto const p = alloc.allocate(n);//分配n个未初始化的string(不会调用string的默认构造函数)
- (2)construct():在allocator分配好的未构造内存上按需构造对象。
- WARNING:为了使用allocate返回的内存,我们必须使用construct构造函数。否则使用未构造的内存,其行为是未定义的
- construct参数:
- 指针参数:接受一个指向未初始化内存的指针,表示在指定位置开始构造
- 零个(或多个)额外参数:用来初始化构造的对象。这些参数必须和需要构造的对象的某一个构造函数的参数类型相匹配。他们跟make_shared的参数类似
#include <memory>
allocator<string> alloc;
auto const p = alloc.allocate(n); //分配n个未初始化的string
auto q = p; //q指向最后已构造的元素之后的位置
alloc.construct(q++); //*q为空字符串
alloc.construct(q++, 10, 'c'); //*q为cccccccccc
alloc.construct(q++, "hi"); //*q为hi
- 还未构造对象的情况下就使用原始内存是错误的!
cout<<*p<<endl;//正确:使用string的输出运算符
cout<<*q<<endl;//灾难:q指向未构造的内存!
- 当使用完对象之后,必须对每个构造的对象调用destroy来销毁他们
- (3)destroy():对已构造的元素进行销毁操作,重新得到未构造内存
- 对于每个构造的元素必须使用destroy来销毁它们。
- WARNING:我们只能对真正构造了的元素进行destroy操作。
- 一旦destroy完成之后,就可以再次使用allocate方法,进行构造对象。
- destroy:接受一个指向构造对象的指针,对指向对象执行析构函数
```C++
while(q!=p)
alloc.destroy(--q);//释放我们真正构造的string
- 一旦元素被销毁后,就可以重新使用这部分内存来保存其他string,也可以归还给系统,释放内存通过deallocate来完成
- (4)**deallocate():释放未构造内存**
- 此函数,将未初始化的内存,返回给系统
- deallocate参数
- 第一个参数指针:不能为空,指向allocate分配的内存——即allocated返回的值
- 第二个参数大小参数,必须与调用allocatee分配内存时提供的大小参数一致。——即当时allocated传递进去的值。
```C++
alloca.deallocate(p,n);
- 拷贝和填充未初始化内存的算法:allocator类定义了两个伴随算法,在未初始化内存中创建对象
- allocator算法:
- 假设b,e是两个指针,并且p是一块内存,uninitialized_copy()表示在p这块内存,创建b到e区间的所有元素。
auto p = alloc.allocate(vi.size() * 2);
uninttialized_copy(b,e,p);
//分配比vi中元素所占用空间大一倍的动态内存
auto p = alloc.allocate(vi.size() * 2);
//通过拷贝vi的元素来构造从p开始的元素
auto q = uninttialized_copy(vi.begin(),vi.end(),p);
//将剩余的元素初始化为42
uninitialized_fill_n(q,vi.size(),42);
- 传递给uninitialized_copy的目的位置迭代器必须指向未构造的内存。与copy不同,uninitialized_copy在给定目的位置构造元素。
- 类似copy,uninitialized_copy返回(递增后的)目的位置迭代器,uninitialized_copy调用会返回一个指针,指向最后一个构造的元素之后的位置。
总结
- 释放内存常指释放动态分配的内存。销毁对象不一定会释放内存
- 理解变量的销毁与其内存的释放之间的关系
- 内置类型的指针在离开作用域时,本身会被销毁,但是其指向的内存空间什么都不会发生,必须以显式的delete进行释放空间。智能指针在离开作用域时,本身也会被销毁,并且计数器减一,当其计数器为0且只有一个智能指针指向该对象时,该对象的内存空间会被释放。如若用智能指针的get()函数得到的一个内置指针来初始化一个临时的智能指针,一旦该内置指针被释放,指向的内存也会被释放,原来的智能指针就会变成空指针
- 假设指向动态内存的普通指针的作用域是某函数且该动态内存只有一个指针指向,如果不在函数内显式delete该指针,该内置指针离开它所在的函数作用域时被销毁时并不会释放动态内存,在函数外由于没有指针指向该动态内存,则该动态内存永远不会被释放。
- 指向动态内存的普通指针离开其作用域被销毁时,该动态内存并不会自动释放
- 动态内存只有通过智能指针或delete才会被释放
- 推荐使用make_shared创建动态内存和返回智能指针,由智能指针自动释放内存(默认地对它管理的指针进行delete操作);最好不要直接用new创建内存和delete释放内存,管理起来很麻烦
- 不要delete智能指针,delete对象是指向动态数组的内置指针。智能指针是一个类,进行动态内存的管理,也保存着指向动态内存的普通指针p.get(),释放内存时默认使用delete销毁内存
- 虽然这一章介绍了动态数组,但一般情况下还是推荐使用容器作为底层。因为使用容器的类可以使用默认版本的拷贝、赋值和析构操作,而分配动态数组的类必须定义自己版本的这些操作,太麻烦了