2.5w字长文爆肝 C++动态内存与智能指针一篇搞懂!太顶了!!!

动态内存与智能指针

  • 1.动态内存与智能指针
  • 2.shared_ptr类
    • 2.1.make_shared函数
    • 2.2.shared_ptr的拷贝和赋值
    • 2.3.shared_ptr自动销毁所管理的对象
    • 2.4. shared_ptr会自动释放相关联的内存
    • 2.5.使用了动态生存期的资源的类
    • 2.6.定义StrBlob类
    • 2.7. StrBlob构造函数
    • 2.8.元素访问成员函数
    • 2.9.StrBlob的拷贝,赋值和销毁
    • 2.9.5StrBlob类的测试
  • 3.直接管理内存
    • 3.1.使用new动态分配和初始化对象
    • 3.2.动态分配的const对象
    • 3.3.释放动态内存
    • 3.4.指针指和delete
    • 3.5.动态对象的生存期直到被释放时为止
    • 3.6.小心:动态内存的管理非常容易出错
    • 3.7.delete之后重置指针值,这只是提供了有限的保护
  • 4.shared_ptr和new结合使用
    • 4.1.不要混合使用普通指针和智能指针
    • 4.2.不要使用get初始化另一个智能指针或为智能指针赋值
    • 4.3.shared_ptr指针操作:reset
    • 4.4.shared_ptr的删除器
  • 5.智能指针和异常
    • 5.1注意:智能指针陷阱
  • 6.unique_ptr
    • 6.1.传递unique_ptr参数和返回
    • 6.2.unique_ptr的删除器
  • 7.weak_ptr
    • 7.1.核查指针类
    • 7.2.指针操作
    • 7.3.StrBlobPtr类的测试
    • 7.4.小插曲:改进StrBlobPtr的构造函数

本篇参考于《C++Primer》

三种内存区别:

  1. 静态存储区
    主要存放static静态变量、全局变量、常量。这些数据内存在编译的时候就已经为他们分配好了内存,生命周期是整个程序从运行到结束。
  2. 栈区
    存放局部变量。在执行函数的时候(包括main这样的函数),函数内的局部变量的存储单元会在栈上创建,函数执行完自动释放,生命周期是从该函数的开始执行到结束。线性结构。
  3. 堆区(存储动态分配的对象)
    程序员自己申请的任意大小的内存。一直存在直到被释放。链表结构。

1.动态内存与智能指针

在C++中,动态内存的管理是通过一对运算符来完成的。

  1. new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;
  2. delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。有时我们会忘记释放内存,在这种情况下就会产生内存泄漏;有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针。

为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:

  1. shared_ptr允许多个指针指向同一个对象;
  2. unique_ptr则“独占”所指向的对象。
  3. 标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。

这三种类型都定义在memory头文件中。

2.shared_ptr类

类似vector,智能指针也是模板,与vector一样,我们在尖括号内给出类型,之后是所定义的这种智能指针的名字:

shared_ptr<string>p1;//shared_ptr,可以指向string
shared_ptr<list<int>>p2;//shared_ptr,可以指向int的list

默认初始化的智能指针中保存着一个空指针,后面我们将介绍初始化智能指针的其他方法。

智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空

下面列出shared_ptr和unique_ptr所支持的操作。

2.5w字长文爆肝 C++动态内存与智能指针一篇搞懂!太顶了!!!_第1张图片
2.5w字长文爆肝 C++动态内存与智能指针一篇搞懂!太顶了!!!_第2张图片图片来源于《C++Primer》

2.1.make_shared函数

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。与智能指针一样,make_shared也定义在头文件memory中。
当要用make_shared时,必须指定想要创建的对象的类型。定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型:

//p3指向一个值为42的int的shared_ptr
shared_ptr<int>p3 = make_shared<int>(42);
//p4指向一个值为“9999999999”的string
shared_ptr<string>p4 = make_shared<string>(10,'9');
//p5指向一个值初始化的int,即值为0
shared_ptr<int>p5 = make_shared<int>();

我们通常用auto定义一个对象来保存make_shared的结果,这种方式比较简单:

//p6指向一个动态分配的空vector
auto p6 = make_shared<vector<string>>();

2.2.shared_ptr的拷贝和赋值

当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:

auto p = make_shared<int>(42);//p指向的对象只有p一个引用者
auto q(p);//p和q指向相同对象,此对象有两个引用者

我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域时,计数器就会递减。
一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:

auto r = make_shared<int>(42);//r指向的int只有一个引用者
r = q;//给r赋值,令它指向另一个地址
//递增q指向的对象的引用计数
//递减r原来指向的对象的引用计数
//r原来指向的对象已没有引用者,会自动释放

此例中我们分配了一个int,将其指针保存在r中。接下来,我们将一个新值赋予r。在此情况下,r是唯一指向此int的shared_ptr,在把q赋给r的过程中,此int被自动释放。

2.3.shared_ptr自动销毁所管理的对象

当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过另一个特殊的成员函数——析构函数完成销毁工作的。

shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。

2.4. shared_ptr会自动释放相关联的内存

当动态对象不再被使用时,shared_ptr类会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。

2.5.使用了动态生存期的资源的类

程序使用动态内存出于以下三种原因之一:
1.程序不知道自己需要使用多少对象
2.程序不知道所需对象的准确类型
3.程序需要在多个对象间共享数据

容器类是出于第一种原因而使用动态内存的典型例子,这里我们拿vector容器举例,每个vector“拥有”其自己的元素。当我们拷贝一个vector时,原vector和副本vector中的元素是相互分离的:

vector<string>v1;//空vector
{
     //新作用域
	vector<string>v2 = {
     "a","an","the"};
	v1 = v2;//从v2拷贝元素到v1中
}//v2被销毁,其中的元素也被销毁
//v1有三个元素,是原来v2中元素的拷贝

由一个vector分配的元素只有当这个vector存在时才存在。当一个vector被销毁时,这个vector中的元素也都被销毁。

出于第二种原因而使用动态内存的情况我们在这里不介绍。

我们现在来定义一个名为Blob的类,保存一组元素。与容器不同,我们希望Blob对象的不同拷贝之间共享相同的元素。即,当我们拷贝一个Blob时,原Blob对象及其拷贝应该引用相同的底层元素。
一般而言,如果两个对象共享底层的数据,当某个对象被销毁时,我们不能单方面地销毁底层数据:

Blob<string>b1;//空Blob
{
     //新作用域
	Blob<string>b2 = {
     "a","an","the"};
	b1 = b2;//b1和b2共享相同的元素
}//b2被销毁了,但b2中的元素不能被销毁
//b1指向最初由b2创建的元素

在此例中,b1和b2共享相同的元素。当b2离开作用域时,这些元素必须保留,因为b1仍然在使用它们。

note:
使用动态内存的一个常见原因是允许多个对象共享相同的状态。

2.6.定义StrBlob类

现在让我们来编写在多个对象间共享数据的类,在这里我们不用模板来实现(因为我还不会模板),因此,现在我们先定义一个管理string的类,此版本命名为StrBlob。
实现一个新的集合类型的最简单方法是使用某个标准库容器来管理元素。采用这种方法,我们可以借助标准库类型来管理元素所使用的内存空间。在本例中,我们将使用vector来保存元素。
但是,我们不能在一个Blob对象内直接保存vector,因为一个对象的成员在对象销毁时也会被销毁。例如,假定b1和b2是两个Blob对象,共享相同的vector。如果此vector保存在其中一个Blob中——例如b2中,那么当b2离开作用域时,此vector也将被销毁,也就是说其中的元素都将不复存在。为了保证vector中的元素继续存在,我们将vector保存在动态内存中。
为了实现我们所希望的数据共享,我们为每个StrBlob设置一个shared_ptr来管理动态分配的vector。此shared_ptr的成员将记录有多少个StrBlob共享相同的vector,并在vector的最后一个使用者被销毁时释放vector。
我们还需要确定这个类应该提供什么操作。当前,我们将实现一个vector操作的小的子集。我们会修改访问元素的操作(如front和back):在我们的类中,如果用户试图访问不存在的元素,这些操作会抛出一个异常。
我们的类有一个默认构造函数和一个构造函数,接受单一的initializer_list类型参数。此构造函数可以接受一个初始化器的花括号列表。

代码如下:

class StrBlob{
     
	public:
		typedef std::vector<std::string>::size_type size_type;
		StrBlob();
		StrBlob(std::initializer_list<std::string>il);
		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();
		std::string&front();
		std::string&back();
	private:
		std::shared_ptr<std::vector<std::string>>data;
		void check(size_type i,const std::string &msg)const;
};

在此类中,我们实现了size、empty和push_back成员。这些成员通过指向底层vector的data成员来完成它们的工作。例如,对一个StrBlob对象调用size()会调用data->size(),依此类推。

2.7. StrBlob构造函数

两个构造函数都使用初始化列表来初始化其data成员,令它指向一个动态分配的vector。默认构造函数分配一个空vector:

StrBlob::StrBlob():data(make_shared<vector<string>>()){
     }
StrBlob::StrBlob(initializer_list<string>il):data(make_shared<vector<string>>(il)){
     }

接受一个initializer_list的构造函数将其参数传递给对应的vector构造函数。此构造函数通过拷贝列表中的值来初始化vector的元素。

2.8.元素访问成员函数

pop_back、front和back操作访问vector中的元素。这些操作在试图访问元素之前必须检查元素是否存在。由于这些成员函数需要做相同的检查操作,我们为StrBlob定义了一个名为check的private工具函数,它检查一个给定索引是否在合法范围内。除了索引,check还接受一个string参数,它会将此参数传递给异常处理程序,这个string描述了错误内容:

pop_back和元素访问成员函数首先调用check。如果check成功,这些成员函数继续利用底层vector的操作来完成自己的工作:

void StrBlob::check(size_type i,const string &msg) const
{
     
	if (i >= data->size())
		throw out_of_range(msg);
}

pop_back和元素访问成员函数首先调用check。如果check成功,这些成员函数继续利用底层vector的操作来完成自己的工作:

string &StrBlob::front()
{
     
	check(0,"front on empty StrBlob");
	return data->front();
}

string &StrBlob::back()
{
     
	check(0,"back on empty StrBlob");
	return data->back();
}

void StrBlob::pop_back()
{
     
	check(0,"pop_back on empty StrBlob");
	data->pop_back();
}

front和back应该对const进行重载

2.9.StrBlob的拷贝,赋值和销毁

StrBlob使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象进行这些操作。默认情况下,这些操作拷贝、赋值和销毁类的数据成员。我们的StrBlob类只有一个数据成员,它是shared_ptr类型。因此,当我们拷贝、赋值或销毁一个StrBlob对象时,它的shared_ptr成员会被拷贝、赋值或销毁。
如前所见,拷贝一个shared_ptr会递增其引用计数;将一个shared_ptr赋予另一个shared_ptr会递增赋值号右侧shared_ptr的引用计数,而递减左侧shared_ptr的引用计数。如果一个shared_ptr的引用计数变为0,它所指向的对象会被自动销毁。因此,对于由StrBlob构造函数分配的vector,当最后一个指向它的StrBlob对象被销毁时,它会随之被自动销毁。

2.9.5StrBlob类的测试

完整代码如下:

#ifndef MY_STRBLOB_H
#define MY_STRBLOB_H
#include 
#include 
#include 
#include 
#include 

using namespace std;

class StrBlob {
     
	public:
		typedef vector<string>::size_type size_type;
		StrBlob();
		StrBlob(initializer_list<string>il);
		size_type size()const {
     
			return data->size();
		}
		bool empty()const {
     
			return data->empty();
		}
		void push_back(const string &t) {
     
			data->push_back(t);
		}
		void pop_back();
		string &front();
		const string &front()const;
		string &back();
		const string &back()const;
	private:
		shared_ptr<std::vector<std::string>>data;
		void check(size_type i, const std::string &msg)const;
};

StrBlob::StrBlob(): data(make_shared<vector<string>>()) {
     
}



StrBlob::StrBlob(initializer_list<string>il): data(make_shared<vector<string>>(il)) {
     }

void StrBlob::check(size_type i, const string &msg)const {
     
	if (i >= data->size())
		throw out_of_range(msg);
}

string &StrBlob::front() {
     
	check(0, "front on empty StrBlob");
	return data->front();
}

const string &StrBlob::front()const {
     
	check(0, "front on empty StrBlob");
	return data->front();
}

string &StrBlob::back() {
     
	check(0, "back on empty StrBlob");
	return data->back();
}

const string &StrBlob::back()const {
     
	check(0, "back on empty StrBlob");
	return data->back();
}

void StrBlob::pop_back() {
     
	check(0, "pop_back on empty StrBlob");
	data->pop_back();
}

#endif

测试代码如下:

#include 
using namespace std;
#include "my_StrBlob.h"

int main() {
     
	StrBlob b1;
	StrBlob b2 = {
     "a", "an", "the"};
	b1 = b2;
	b2.push_back("about");
	cout << b2.size() << endl;
	cout << b1.size() << endl;
	cout << b1.front() << " " << b1.back() << endl;

	const StrBlob b3 = b1;
	cout << b3.front() << " " << b3.back() << endl;
	return 0;
}

测试结果:
2.5w字长文爆肝 C++动态内存与智能指针一篇搞懂!太顶了!!!_第3张图片
普通vector代码如下:

#include 
#include 
using namespace std;

int main()
{
     
	vector<string>b1;
		vector<string> b2 = {
     "a", "an", "the"};
		b1 = b2;
		b2.push_back("about");
		cout << b2.size() << endl;
	cout << b1.size() << endl;
	cout << b1.front() << " " << b1.back() << endl;

	const vector<string> b3 = b1;
	cout << b3.front() << " " << b3.back() << endl;
	return 0;
}

测试结果:
在这里插入图片描述

3.直接管理内存

C++定义了new分配内存,delete释放new分配的内存。

相对于智能指针,使用这两个运算符管理内存非常容易出错

而且,自己直接管理内存的类与使用智能指针的类不同,它们不能依赖类对象拷贝、赋值和销毁操作的任何默认定义。因此,使用智能指针的程序更容易编写和调试。

3.1.使用new动态分配和初始化对象

在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针:

int *pi = new int;//pi指向一个动态分配的,未初始化的无名对象

此new表达式在自由空间构造一个int型对象,并返回指向该对象的指针。

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

string *ps = new string;//初始化空string
int *pi = new int;//pi指向一个未初始化的int

初始化方式(1.使用圆括号,2.使用列表初始化,3.在类型名之后跟一对空括号进行值初始化):

int *pi = new int(1024);//pi指向的对象的值为1024
string *ps = new string(10,'9');//*ps为"9999999999"
vector<int>*pv = new vector<int>{
     0,1,2,3,4,5,6,7,8,9};
//vector有10个元素,值依次从0到9

string *ps1 = new string;//默认初始化为空string
string *ps = new string();//值初始化为空string
int *pi1 = new int;//默认初始化;*pi1的值未定义
int *pi2 = new int();//值初始化为0,*pi2为0

如果我们用auto来初始化,由于编译器要用初始化器的类型来推断要分配的类型,只有当括号中仅有单一初始化器时才可以使用auto:

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

p1的类型是一个指针,指向从obj自动推断出的类型。若obj是一个int,那么p1就是int*;若obj是一个string,那么p1是一个string*;依此类推。新分配的对象用obj的值进行初始化。

3.2.动态分配的const对象

用new分配const对象是合法的:

const int *pci = new const int(1024);
//分配并初始化一个const int
const string *pcs = new const string;
//分配并默认初始化一个const的空string

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

3.3.释放动态内存

为了防止内存耗尽(在这里不介绍),在动态内存使用完毕后,必须将其归还给系统。我们通过delete表达式来将动态内存归还给系统。delete表达式接受一个指针,指向我们想要释放的对象:

delete p;//p必须指向一个动态分配的对象或是一个空指针

note:
delete p; 之后,p的值(指向的地址不变),但不能再使用p处理该地址的内容,也不能再delete p,因为delete之后,p不指向nullptr。
通常需要delete之后置指针为空:
delete p;
p = nullptr;


与new类型类似,delete表达式也执行两个动作:销毁给定的指针指向的对象;释放对应的内存。

3.4.指针指和delete

我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存,或者将相同的指针值释放多次,又或者释放一个局部变量等,其行为是未定义的。

举个例子:
如果我们delete了局部变量,那么当局部变量离开了它的作用域,系统要销毁这个局部变量!可这个局部变量已经被我们销毁了,就相当于将相同的指针值释放多次。(如果错误,还请指正)

虽然一个const对象的值不能被改变,但它本身是可以被销毁的。

const int *pci = new const int(1024);
delete pci;

3.5.动态对象的生存期直到被释放时为止

注意:调用者必须记得释放内存

让我们看下面这段代码:

#include 
using namespace std;


int *fff(int a) //返回一个指针,指向一个动态分配的对象
{
     
	return new int(a);
}

void use_fff(int a) {
     
	int *b = fff(a);//调用者必须记得释放此内存
	cout << *b << endl;
}//b离开了它的作用域,但它所指向的内存没有被释放!!!

int main() {
     
	use_fff(42);
	return 0;
}

注意:
由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在。

#include 
using namespace std;


int *fff(int a) {
      //返回一个指针,指向一个动态分配的对象
	return new int(a);
}

void use_fff(int a) {
     
	int *b = fff(a);//调用者必须记得释放此内存
	cout << *b << endl;
	delete b;//现在记得释放内存,我们已经不需要它了
}

int main() {
     
	use_fff(42);
	return 0;
}

当然,如果在这段代码中,我们不释放内存,那么目的就是,我们的系统中的其他代码要使用use_fff所分配的对象,我们就应该修改此函数,让它返回一个指针,指向它分配的内存:

#include 
using namespace std;


int *fff(int a) {
      //返回一个指针,指向一个动态分配的对象
	return new int(a);
}

int *use_fff(int a) {
     
	int *b = fff(a);//调用者必须记得释放此内存
	cout << *b << endl;
	return b;
}

int main() {
     
	int *c = use_fff(42);
	cout << *c << endl;
	return 0;
}

3.6.小心:动态内存的管理非常容易出错

  1. 忘记delete内存
  2. 使用已释放掉的内存
  3. 同一块内存释放两次

坚持只使用智能指针,就可以避免所有这些问题。对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它。

3.7.delete之后重置指针值,这只是提供了有限的保护

当我们delete一个指针后,指针值就变为无效了。虽然指针无效,但是很多机器上指针(已经释放了的)仍然保存着动态内存的地址。在delete之后,指针就变成了空悬指针

空悬指针:指向一块曾经保存数据对象但现在已经无效的内存的指针。

未初始化指针的所有缺点空悬指针也都有。有一种方法可以避免空悬指针的问题:在指针即将要离开其作用域之前释放掉它所关联的内存。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。如果我们需要保留指针,可以在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。

但这也仅仅提供了有限的保护。

比如下面这个例子:

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也变为无效了。在实际系统中,查找指向相同内存的所有指针是异常困难的。(在这里,我们很难发现q已经无效了)

4.shared_ptr和new结合使用

如果我们不初始化一个智能指针,它就会被初始化为一个空指针,如下面代码所示,我们还可以用new返回的指针来初始化智能指针:

shared_ptr<double>p1;//shared_ptr可以指向一个double
shared_ptr<int>p2(new int(42));//p2指向一个值为42的int

接受指针参数的智能指针构造函数是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);
	//错误;隐式转换为shared_ptr;
}

shared_ptr<int>clone(int p)
{
     
	return shared_ptr<int>(new int(p));
	//正确;显式地用int*创建shared_ptr;
}

默认情况下,

  1. 一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。
  2. 我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代delete。

2.5w字长文爆肝 C++动态内存与智能指针一篇搞懂!太顶了!!!_第4张图片
2.5w字长文爆肝 C++动态内存与智能指针一篇搞懂!太顶了!!!_第5张图片

4.1.不要混合使用普通指针和智能指针

shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ptr)之间。这也是为什么我们推荐使用make_shared而不是new的原因。这样,我们就能在分配对象的同时就将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上

现在让我们看下面这段代码:

//在函数调用时ptr被创建并初始化
void process(shared_ptr<int>ptr)
{
     
	//使用ptr
};//ptr离开作用域,被销毁

process的参数是传值方式传递,因此实参会被拷贝到ptr,创建一个shared_ptr会递增其引用计数,拷贝一个shared_ptr也会递增其引用计数,因此在process运行过程中,其引用计数至少为2,等ptr离开作用域,ptr的引用计数会递减,但不会变为0。因此,当局部变量ptr被销毁时,ptr指向的内存不会被释放。

使用process函数的正确方法是传递给它一个shared_ptr:

shared_ptr<int>p(new int(42));//引用计数为1
process(p);//拷贝p会递增引用计数,引用计数为2
int i = *p;//正确,引用计数为1

现在让我们混合使用普通指针和智能指针,看会发生什么???

虽然我们不能传递给process一个内置指针,但可以传递给它一个(临时的)shared_ptr,这个shared_ptr是用一个内置指针显式构造的。但是,这样做很可能会导致错误:

int *x(new int(1024));//危险,x是一个普通指针,不是一个智能指针
process(x);//错误,不能将int*转换为一个shared_ptr
process(shared_ptr<int>(x));//合法的,但内存会被释放
int j = *x;//未定义,x是一个空悬指针

在上面的调用中,我们将一个临时shared_ptr传递给process。当这个调用所在的表达式结束时,这个临时对象就被销毁了。销毁这个临时变量会递减引用计数,此时引用计数就变为0了。因此,当临时对象被销毁时,它所指向的内存会被释放。
但x继续指向(已经释放的)内存,从而变成一个空悬指针。如果试图使用x的值,其行为是未定义的。
当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。

注意:
使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。

4.2.不要使用get初始化另一个智能指针或为智能指针赋值

智能指针类型定义了一个名为get的函数(参见表12.1),它返回一个内置指针,指向智能指针管理的对象。

当我们需要向不能使用智能指针的代码传递一个内置指针时,我们才会用get。

使用get返回的指针的代码不能delete此指针。

虽然编译器不会给出错误信息,但将另一个智能指针也绑定到get返回的指针上是错误的:

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和q指向相同的内存。由于它们是相互独立创建的,因此各自的引用计数都是1。当q所在的程序块结束时,q被销毁,这会导致q指向的内存被释放。从而p变成一个空悬指针,意味着当我们试图使用p时,将发生未定义的行为。而且,当p被销毁时,这块内存会被第二次delete。

注意:
get用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,才能使用get。特别是,永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。

4.3.shared_ptr指针操作:reset

上面的代码,指针p指向的内存已经被释放了,不过我们可以用reset来将一个新的指针赋予一个shared_ptr:

p = new int(1024);//错误,不能将一个指针赋予shared_ptr
p.reset(new int(1024));//正确,p指向一个新对象

与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象。**reset成员经常与unique一起使用,来控制多个shared_ptr共享的对象。**在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:

if (!p.unique())
	p.reset(new string(*p));//我们不是唯一用户;分配新的拷贝
*p+=newVal;//现在我们知道自己是唯一的用户,可以改变对象的值

4.4.shared_ptr的删除器

本文暂时未介绍!!!

5.智能指针和异常

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

void f()
{
     
	shared_ptr<int>sp(new int(42));//分配一个对象
	//发生异常,且未被f捕获
}//在函数结束时shared_ptr自动释放内存

函数的退出有两种可能,正常处理结束或者发生了异常,无论哪种情况,局部对象都会被销毁。在上面的程序中,sp是一个shared_ptr,因此sp销毁时会检查引用计数。在此例中,sp是指向这块内存的唯一指针,因此内存会被释放掉。
与之相对的,当发生异常时,我们直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放:

void f
{
     
	int *ip = new int(42);//动态分配一个新对象
	//发生异常,且在f中未被捕获
	delete ip//在退出之前手动释放内存
}

如果在new和delete之间发生异常,且异常未在f中被捕获,则内存就永远不会被释放了。在函数f之外没有指针指向这块内存,因此就无法释放它了。

5.1注意:智能指针陷阱

智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范:

  1. · 不使用相同的内置指针值初始化(或reset)多个智能指针。
  2. · 不delete get()返回的指针。
  3. · 不使用get()初始化或reset另一个智能指针。
  4. · 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
  5. ·如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器(本文暂时没有介绍该内容)

6.unique_ptr

一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。表12.4列出了unique_ptr特有的操作。与shared_ptr相同的操作列在表12.1)中。

2.5w字长文爆肝 C++动态内存与智能指针一篇搞懂!太顶了!!!_第6张图片

与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化unique_ptr必须采用直接初始化形式:

unique_ptr<double>p1;//可以指向一个double的unique_ptr
unique_ptr<int>p2(new int(42));//p2指向一个值为42的int

由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作:

unique_ptr<string>p1(new string("Stegosaurus"));
unique_ptr<string>p2(p1);//错误,unique_ptr不支持拷贝
unique_ptr<string>p3;
p3 = p2;//错误,unique不支持赋值

虽然我们不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique:

//将所有权从p1(指向string Stegosaurus)转移给p2
unique_ptr<string>p2(p1.release());//release将p1置为空
unique_ptr<string>p3(new string("Trex"));
//将所有权从p3转移给p2
p2.reset(p3.release());//reset释放了p2原来指向的内存
  1. release成员返回unique_ptr当前保存的指针并将其置为空。
  2. reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。如果unique_ptr不为空,它原来指向的对象被释放。
unique_ptr<string>p2(p1.release());

因此,p2被初始化为p1原来保存的指针,而p1被置为空。

p2.reset(p3.release());

因此,对p2调用reset释放了用"Stegosaurus"初始化的string所使用的内存,将p3对指针的所有权转移给p2,并将p3置为空。

调用release会切断unique_ptr和它原来管理的对象间的联系。**release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。**在本例中,管理内存的责任简单地从一个智能指针转移给另一个。但是,如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:

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

6.1.传递unique_ptr参数和返回

unique_ptr不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr:

unique_ptr<int>clone(int p)
{
     
	//正确,从int*创建一个unique_ptr
	return unique_ptr<int>(new int(p));
}

还可以返回一个局部对象的拷贝:

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

对于两段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的“拷贝”(本文无介绍)

6.2.unique_ptr的删除器

本文暂时不介绍!!!

7.weak_ptr

weak_ptr(见表12.5)是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放,因此,weak_ptr的名字抓住了这种智能指针“弱”共享对象的特点。
2.5w字长文爆肝 C++动态内存与智能指针一篇搞懂!太顶了!!!_第7张图片当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:

auto p = make_shared<int>(42);
weak_ptr<int>wp(p);//wp弱共享p;p的引用计数未改变

本例中wp和p指向相同的对象。由于是弱共享,创建wp不会改变p的引用计数;wp指向的对象可能被释放掉。
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock此函数检查weak_ptr指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的shared_ptr。与任何其他shared_ptr类似,只要此shared_ptr存在,它所指向的底层对象也就会一直存在。例如:

auto p = make_shared<int>(42);
weak_ptr<int>wp(p);//wp弱共享p;p的引用计数未改变
if(shared_ptr<int>np = wp.lock())//如果np不为空则条件成立
{
     
	//在if中,np与p共享对象
}

在这段代码中,只有当lock调用返回true时我们才会进入if语句体。在if中,使用np访问共享对象是安全的。

7.1.核查指针类

作为weak_ptr用途的一个展示,我们将为StrBlob类定义一个伴随指针类。我们的指针类将命名为StrBlobPtr,会保存一个weak_ptr,指向StrBlob的data成员,这是初始化时提供给它的。通过使用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){
     }
		std::string &deref()const;
		StrBlobPtr&incr();//前缀递增
	private:
		//若检查成功,check返回一个指向vector的shared_ptr
		std::shared_ptr<std::vector<std::string>>
		check(std::size_t,const std::string&)const;
		//保存一个weak_ptr,意味着底层vector可能会被销毁
		std::weak_ptr<std::vector<std::string>>wptr;
		std::size_t curr;//在数组中的当前位置
};

默认构造函数生成一个空的StrBlobPtr。其构造函数初始化列表将curr显式初始化为0,并将wptr隐式初始化为一个空weak_ptr。第二个构造函数接受一个StrBlob引用和一个可选的索引值。此构造函数初始化wptr,令其指向给定StrBlob对象的shared_ptr中的vector,并将curr初始化为sz的值。我们使用了默认参数,表示默认情况下将curr初始化为第一个元素的下标。我们将会看到,StrBlob的end成员将会用到参数sz。

值得注意的是,我们不能将StrBlobPtr绑定到一个const StrBlob对象。这个限制是由于构造函数接受一个非const StrBlob对象的引用而导致的
StrBlobPtr的check成员与StrBlob中的同名成员不同,它还要检查指针指向的vector是否还存在:

std::shared_ptr<std::vector<std::string>>
StrBlobPtr::check(std::size_t i,const std::string &msg) const
{
     
	auto ret = wptr.lock();//vector还存在吗?
	if (!ret)
		throw std::runtime_error("unbound StrBlobPtr");
	if (i >= ret->size())
		throw std::out_of_range(msg);
	return ret;	//否则,返回指向vector的shared_ptr
}

由于一个weak_ptr不参与其对应的shared_ptr的引用计数,StrBlobPtr指向的vector可能已经被释放了。如果vector已销毁,lock将返回一个空指针。在本例中,任何vector的引用都会失败,于是抛出一个异常。否则,check会检查给定索引,如果索引值合法,check返回从lock获得的shared_ptr。

7.2.指针操作

在这里我们不用重载运算符的方法,我们定义名为deref和incr的函数,分别用来解引用和递增StrBlobPtr。
deref成员调用check,检查使用vector是否安全以及curr是否在合法范围内:

std::string&StrBlobPtr::deref()const
{
     
	auto p = check(curr,"dereference past end");
	return (*p)[curr];//(*p)是对象所指向的vector
}

如果check成功,p就是一个shared_ptr,指向StrBlobPtr所指向的vector。表达式(*p)[curr]解引用shared_ptr来获得vector,然后使用下标运算符提取并返回curr位置上的元素。

//incr成员也调用check
//前缀递增;返回递增后的对象的引用
StrBlobPtr&StrBlobPtr::incr()
{
     
	//如果curr已经指向容器的尾后位置,就不能递增它
	check(curr,"increment past end of StrBlobPtr");
	++curr;//推进当前位置
	return *this;
}

当然,为了访问data成员,我们的指针类必须声明为StrBlob的friend(参见7.3.4节,第250页)。我们还要为StrBlob类定义begin和end操作,返回一个指向它自身的StrBlobPtr:

//对于StrBlob中的友元声明来说,此前置声明是必要的
class StrBlobPtr;
class StrBlob
{
     
	friend class StrBlobPtr;
	//返回指向首元素和后元素的StrBlobPtr
	StrBlobPtr begin(){
     return StrBlobPtr (*this);}
	StrBlobPtr end()
	{
     
		auto ret = StrBlobPtr(*this,data->size());
		return ret;
	}
};

7.3.StrBlobPtr类的测试

完整代码如下:

#ifndef MY_STRBLOB_H
#define MY_STRBLOB_H
#include 
#include 
#include 
#include 
#include 

using namespace std;

//提前声明,StrBlob中的友类声明所需
class StrBlobPtr;

class StrBlob {
     
		friend class StrBlobPtr;
	public:
		typedef vector<string>::size_type size_type;
		StrBlob();
		StrBlob(initializer_list<string>il);
		size_type size()const {
     
			return data->size();
		}
		bool empty()const {
     
			return data->empty();
		}
		//添加和删除元素
		void push_back(const string &t) {
     
			data->push_back(t);
		}
		void pop_back();
		//元素访问
		string &front();
		const string &front()const;
		string &back();
		const string &back()const;
		
		//提供给StrBlobPtr的接口
		StrBlobPtr begin();//这是声明,定义StrBlobPtr后才能定义这两个函数
		StrBlobPtr end();
	private:
		shared_ptr<std::vector<std::string>>data;
		//如果data[i]不合法,抛出一个异常
		void check(size_type i, const std::string &msg)const;
};

inline StrBlob::StrBlob(): data(make_shared<vector<string>>()) {
     }

StrBlob::StrBlob(initializer_list<string>il): data(make_shared<vector<string>>(il)) {
     }

inline void StrBlob::check(size_type i, const string &msg) const {
     
	if (i >= data->size())
		throw out_of_range(msg);
}

inline string&StrBlob::front()
{
     
	//如果vector为空,check会抛出一个异常
	check(0,"front on empty StrBlob");
	return data->front();
}

//const版本front
inline const string &StrBlob::front()const {
     
	check(0, "front on empty StrBlob");
	return data->front();
}

inline string &StrBlob::back() {
     
	check(0, "back on empty StrBlob");
	return data->back();
}

//const 版本 back
inline const string &StrBlob::back()const {
     
	check(0, "back on empty StrBlob");
	return data->back();
}

inline void StrBlob::pop_back() {
     
	check(0, "pop_back on empty StrBlob");
	data->pop_back();
}

//当试图访问一个不存在的元素时,StrBlobPtr抛出一个异常
class StrBlobPtr {
     
		friend bool eq(const StrBlobPtr &, const StrBlobPtr &);
	public:
		StrBlobPtr(): curr(0) {
     }
		StrBlobPtr(StrBlob &a, size_t sz = 0): wptr(a.data), curr(sz) {
     }
		string &deref()const;
		StrBlobPtr &incr();//前缀递增
		StrBlobPtr &decr();//前缀递减
	private:
		//若检查成功,check返回一个指向vector的shared_ptr
		shared_ptr<vector<string>>
		                        check(size_t, const string &)const;

		//保存一个weak_ptr,意味着底层vector可能会被销毁
		weak_ptr<vector<string>>wptr;
		size_t curr;//在数组中的当前位置
};

inline
shared_ptr<vector<string>>
StrBlobPtr::check(size_t i, const string &msg)const {
     
	auto ret = wptr.lock();//vector还存在吗?
	if (!ret)
		throw runtime_error("unbound StrBlobPtr");
	if (i >= ret->size())
		throw out_of_range(msg);
	return ret;
}

inline string &StrBlobPtr::deref()const {
     
	auto p = check(curr, "dereference past end");
	return (*p)[curr];
}

//前缀递增:返回递增后的对象的引用
inline StrBlobPtr &StrBlobPtr::incr() {
     
	//如果curr已经指向容器的尾后位置,就不能递增它
	check(curr, "increment past end of StrBlobPtr");
	++curr;//推进当前位置
	return *this;
}

//前缀递减:返回递减后的对象的引用
inline StrBlobPtr &StrBlobPtr::decr() {
     
	//如果curr已经为0,递减它就会产生一个非法下标
	--curr;						//递减当前位置
	check(-1, "decrement past begin of StrBlobPtr");
	return *this;
}

//StrBlob的begin和end成员的定义
inline StrBlobPtr StrBlob::begin() {
     
	return StrBlobPtr(*this);
}

inline StrBlobPtr StrBlob::end() {
     
	auto ret = StrBlobPtr(*this, data->size());
	return ret;
}


//StrBlobPtr的比较操作
inline
bool eq(const StrBlobPtr &lhs, const StrBlobPtr &rhs) {
     
	auto l = lhs.wptr.lock(), r = rhs.wptr.lock();
	//若底层的vector是同一个
	if (l == r)
		//则两个指针都是空,或者指向相同的元素时,它们相等
		return (!r || lhs.curr == rhs.curr);
	else
		return false;//若指向不同vector,则不可能相等
}


inline
bool neq(const StrBlobPtr &lhs, const StrBlobPtr &rhs) {
     
	return !eq(lhs, rhs);
}
#endif

主程序代码:

#include 
using namespace std;
#include "my_StrBlob.h"

int main(int argc, char **argv) {
     
	StrBlob b1;
	{
     
		StrBlob b2 = {
     "a", "an", "the"};
		b1 = b2;
		b2.push_back("about");
		cout << b2.size() << endl;
	}
	cout << b1.size() << endl;
	cout << b1.front() << " " << b1.back() << endl;
	const StrBlob b3 = b1;
	                   cout << b3.front() << " "<<b3.back() << endl;

	for (auto it = b1.begin(); neq(it, b1.end()); it.incr())
		cout << it.deref() << endl;
	return 0;
}

测试结果:

2.5w字长文爆肝 C++动态内存与智能指针一篇搞懂!太顶了!!!_第8张图片

这次我们用文件读入的方式测试:

2.5w字长文爆肝 C++动态内存与智能指针一篇搞懂!太顶了!!!_第9张图片

代码如下:

#include 
#include 
using namespace std;

#include "my_StrBlob.h"

int main(int argc, char **argv) {
     
	ifstream in(argv[1]);
	if (!in) {
     
		cout << "无法打开输入文件" << endl;
		return -1;
	}
	StrBlob b;
	string s;
	while (getline(in, s))
		b.push_back(s);

	for (auto it = b.begin(); neq(it, b.end()); it.incr())
		cout << it.deref() << endl;
	system("pause");
	return 0;
}

2.5w字长文爆肝 C++动态内存与智能指针一篇搞懂!太顶了!!!_第10张图片

7.4.小插曲:改进StrBlobPtr的构造函数

我们不能将StrBlobPtr绑定到一个const StrBlob对象。这个限制是由于构造函数接受一个非const StrBlob对象的引用而导致的

现在我们来设计一个可以接受const StrBlob对象的引用的构造函数。

首先,为StrBlobPtr定义能接受const StrBlob &参数的构造函数:

StrBlob(const StrBlob &a,size_t sz = 0):wptr(a.data),curr(sz){
     }

其次,为StrBlob定义能操作const对象的begin和end。

声明:

StrBlobPtr begin() const;
StrBlobPtr end() const;

定义:

inline StrBlobPtr StrBlob::begin()const
{
     
	return StrBlobPtr(*this);
}

inline StrBlobPtr StrBlob::end()const
{
     
	auto ret = StrBlobPtr(*this,data->size());
	return ret;
}
  1. 本人水平有限,如有错误,还请在评论区留言指出,谢谢!!
  2. 创作不易,留个赞再走呗!!!

你可能感兴趣的:(C++学习之路,C++,C++Primer,动态内存,智能指针,C++11)