《c++ primer》 第12章 动态内存 学习笔记

概述

静态内存:保存static变量和全局变量。它在程序结束后自行销毁。
栈内存:保存局部非static变量,它在程序块接受后自行销毁。
堆内存:不会自动释放,需要手动释放。

12.1动态内存与智能指针

c++中动态内存的管理是通过一对运算符来完成的:
new:在动态内存中对对象分配空间并返回一个指向该对象的指针,我们可以对该对象进行初始化。
delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
有时我们会忘记释放它,造成内存泄漏。
有时还在使用这个动态对象指针时就释放了它,造成引用非法内存的指针。

C++11新标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,区别是它自动释放所指向的内存。

它们定义在头文件#include <memory>

两种智能指针:

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

unique_ptr:独占所指向的对象。

伴随类weak_ptr:弱引用,指向share_ptr所管理的对象。

12.1.1 share_ptr类

shared_ptr和unique_ptr都支持的操作 
shared_ptr<string>sp; //空智能指针。可以指向string类型的对象 
unique_ptr<string>up;  
sp         //sp可以作为条件判断sp是否指向一个对象  
*sp        //解引用sp,获得它指向的对象  
sp->mem    //等价于(*sp).mem  
sp.get()   //返回sp中所报存的指针。要小心使用,所智能指针释放了对象,则返回的指针所指向的对象也不存在了。  
swap(sp,sq)  
sp.swap(sq)  //交换sp和sq中的指针  
  
shared_ptr支持的操作  
make_shared<T>(args)  //返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化对象  
shared_ptr<T>p(q)     //p是shared_ptr q的拷贝,此操作会递增q中的记数器,q中的指针必须能转换成T*  
p = q                 //都是shared_ptr,保存的指针必须能相互转换,操作会递减p的引用计数,增加q的引用计数,p引用计数为0时会释放其管理的内存  
p.use_count()         //返回与p共享智能指针的数量,可能很慢主要用于调试  
p.unique()            //当p.use_count()为1时,返回ture,否则返回false。  
 

初始化(构造函数)

shared_ptr<string>sp;//默认构造函数,空指针
shared_ptr<T>p(q)     //p是shared_ptr q的拷贝,此操作会递增q中的记数器,q中的指针必须能转换成T* 
make_shared<T>(args)  //返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化对象  
**注意:make_shared<T>(args)中      T类类型时:args必须与T的某个构造函数相匹配。 T为内置类型时:args必须能用来初始化一个T

赋值拷贝p = q ;

swap(sp,sq);
p= make_shared<T>(args);

#include <iostream>
#include <string>
#include <vector>
#include <memory>

using namespace std;

int main()
{
	shared_ptr<string> sp;
	//cout << sp.use_count;//提示出错。因为没有指向任何对象
	make_shared<string>(); //动态分配内存默认初始化,调用默认构造函数必须要有括号
	make_shared<string>("a"); //动态分配内存值初始化
	shared_ptr<string>sp2 = make_shared<string>();//初始化智能指针
	shared_ptr<string>sp3 = make_shared<string>("b");//初始化智能指针
	cout << sp3.use_count() << endl;
	auto sp4 = sp3;//拷贝一次观察计数器变化
	cout << sp3.use_count() << endl;
	sp4 = sp2;//被其他赋值,观察计数器变化
	cout << sp3.use_count() << endl;
	getchar();
}

程序使用动态内存出于以下三种原因

1.程序不知道自己需要使用多少对象

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

3.程序需要在多个对象间共享数据。

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

例子:我们希望定义一个Blob类,保存一组元素,与容器不同,我们希望Blob对象的不同拷贝之间共享相同的元素。既当我们拷贝一个Blob时,

原Blob对象及其拷贝应该引用相同的底层元素。

定义一个管理string的类,命名为StrBlob。

#include <iostream>
#include <string>
#include <memory>             //智能指针和动态分配内存
#include <vector>
#include <initializer_list>   //初始值列表
#include <stdexcept>

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() { return data->empty(); }
	//添加删除元素
	void push_back(const std::string &s){ data->push_back(s); }
	void pop_back();
	//访问元素
	std::string& front();
	std::string& back();
	const std::string& front()const;
	const std::string& back() const;

private:
	std::shared_ptr<std::vector<std::string>> data;//智能指针,指向string的vector
	//private 检查函数,当我们访问容器时检查下标是否越界
	void check(size_type i, const std::string &msg)const;
};

//构造函数
StrBlob::StrBlob() :
data(std::make_shared<std::vector<std::string>>()) { }

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

//检查函数
void StrBlob::check(size_type i, const std::string &msg)const
{
	if (i >= data->size())//不在范围内
		throw std::out_of_range(msg);
}

//重载函数:const和non_const是因为有时候有const StrBlob对象,此时需要用const函数
//调用const版本时对象是const,所以this指针也是const,通过转换this指针才能调用const版本,

std::string& StrBlob::back()
{

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

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

//const版本的,当const StrBlob A时,数据成员也是const的
const std::string& StrBlob::back() const
{
	check(0, "back on empty StrBlob");
	return data->back();
}

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

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

int main()
{
	std::shared_ptr<StrBlob> sp;//空指针
	std::cout << sp << std::endl;
	StrBlob s({ "zhang", "yang"});
	StrBlob s2(s);//共享s内的数据
	std::string st = "abcd";
	s2.push_back(st);	
	std::cout << s2.front() << std::endl;
	std::cout << s2.back() << std::endl;
}


12.1.2直接管理内存

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

int *p=new int;

直接初始化方式:int *p=new int(1024);

值初始化方式:int *p=new int();

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

动态内存的管理非常容易出错

1,忘记delete内存

2,使用已经释放掉的对象

3,同一块内存释放两次

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

delete之后重置指针

delete p;之后,指针值变得无效,但是指针仍然保存着动态内存的地址,此时的指针叫做空悬指针。可以在delete之后将nullptr赋予给指针。

12.1.3shared_ptr和new结合使用

如果我们不初始化一个智能指针,它就会被初始化为一个空指针。

我们可以用new返回的指针来初始化智能指针。接受指针参数的智能指针构造函数是explicit(避免隐式转换)的,我们不能将一个内置指针隐式转换为一个智能指针。

shared_ptr<int>p(new int(10)); //正确,调用了构造函数来初始化的 
shared_ptr<int>p=new int(10);  //错误,int *隐式转换为share_ptr类型的(构造函数接受int *),然后在初始化


不要混用智能指针和普通指针

当将一个share_ptr绑定到一个普通指针时,我们将内存的管理责任交给了这个share_ptr。一旦这样做了,我们就不应该在使用内置指针来访问share_ptr所指向的内存了。

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

智能指针定义了一个名为get的函数,返回一个普通类型的指针。eg:p.get() 返回一个int *

目的:向不能使用智能指针的代码传递一个内置指针,使用get返回的指针的代码不能delete此指针。

将另一个智能指针绑定到get返回的指针也是错误的。

永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。

普通指针不能自动转化为智能指针。


p.get()       //返回p中的指针,为内置类型

p.reset()    //p是唯一指向其对象的share_ptr,reset会释放此对象,置空p

p.reset(q)  //若q为内置类型,令p指向q


所以为了正确使用智能指针,我们必须坚持一些基本规范:

1,不使用相同的内置指针初始化(或reset)多个智能指针。

2,不delete get()返回的指针

3,不使用get()初始化或reset另一个智能指针

4,如果使用了get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变得无效了。

5,如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。


12.1.5unique_ptr

一个unique_ptr 拥有它所指向的对象,和shared_ptr不同,某个时刻只能有一个unique_ptr 指向一个给定对象,当unique_ptr 被销毁时,对象也被销毁。

初始化:只支持new内置类型的初始化

 unique_ptr<int>q(new int(p));  //正确

 unique_ptr<int>q;//正确

 unique_ptr<int>q=new int(p); 错误,不支持隐式初始化  

unique没有类似make_shared,必须手动new,将其绑定。由于unique_ptr独占它所指向的对象,因此他不支持普通的拷贝和赋值

但是有种特殊的拷贝可以支持:我们可以拷贝或赋值一个即将要被销毁的unique_ptr。

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

u.release();//u放弃了对指针的控制权,返回内置指针,并将u清空

u.reset();//释放u指向的对象,u不需要担心,因为程序结束u自动清空。

u.reset(q);//若q为内置指针,令u指向这个对象

#include<iostream>
#include<memory>

using namespace std;

int main()
{
	unique_ptr<int> u1(new int(1));
	cout << *u1 << endl;
	//不能直接赋值,只能转移值
	//前提是要先释放,然后给另一个指针赋值
	//用到u.release()和u.reset()函数
	unique_ptr<int> u2(u1.release());//u1释放自己,赋值给u2
	cout << *u2 << endl;
	unique_ptr<int> u3;
	u3.reset(u2.release());//u2释放自己,赋值给u3
	cout << *u3 << endl;
}

注意: p.release()返回一个指针,并将p置空,若没有为对象赋值,则出现错误。

p.release( );               //错误:p被置空,并返回了一个内置指针,但是内存泄漏了
auto q = p.release( );      //q 是int * 类型, 记得delete释放q

12.1.6weak_ptr


weak_ptr 是一种不控制对象生存期的智能指针,它指向由一个shared_ptr 管理的对象。所以当我们创建一个weak_ptr 必须用一个 shared_ptr 初始化。

引入lock和expired是防止在weak_ptr 不知情的情况下,shared_ptr 被释放掉

weak_ptr 不会更改shared_ptr 的引用计数。

std::weak_ptr 是一种智能指针,它对被 std::shared_ptr 管理的对象存在非拥有性(“弱”)引用。在访问所引用的对象前必须先转换为 std::shared_ptr

std::weak_ptr 用来表达临时所有权的概念:当某个对象只有存在时才需要被访问,而且随时可能被他人删除时,可以使用std::weak_ptr 来跟踪该对象。需要获得临时所有权时,则将其转换为 std::shared_ptr,此时如果原来的 std::shared_ptr被销毁,则该对象的生命期将被延长至这个临时的 std::shared_ptr 同样被销毁为止。

此外,std::weak_ptr 还可以用来避免 std::shared_ptr 的循环引用。

/*
 *避免拷贝,多个指针共用一个vector<string>
 *使用weak_ptr访问共享的对象
 * */
#include <iostream>
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <stdexcept>
#include <fstream>
#include <sstream>

class StrBlob;
class StrBlobPtr;


class StrBlob
{
	public:
		friend class StrBlobPtr;
		typedef std::vector<std::string>::size_type size_type;
		StrBlob(); //默认构造函数
		StrBlob(std::initializer_list<std::string>il); //拷贝构造函数
		size_type size() { return data->size(); }      //对data进行解引用就是对vector<string>操作
		std::string& front();
		std::string& back();
		const std::string& front()const;
		const std::string& back()const;
		void push_back(const std::string &s) { data->push_back(s); }
		void pop_back();
		StrBlobPtr begin();
		StrBlobPtr end() ;

	private:
		void check(size_type sz, std::string msg)const;
		std::shared_ptr<std::vector<std::string>>data;
};

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

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

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

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

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

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

		/* --------------------------------------------------------------------------------- */

		//必须定义在StrBlobPtr的后面
		class StrBlobPtr
{
	public:
		friend StrBlob;
		StrBlobPtr() :curr(0){ }
		StrBlobPtr(StrBlob &s, std::size_t sz = 0) :
			wptr(s.data), curr(sz){ }
		std::string& deref()const;//返回当前string
		StrBlobPtr& incr(); //递增

	private:
		std::shared_ptr<std::vector<std::string>> check(std::size_t i, const std::string &msg)const;
		std::weak_ptr<std::vector<std::string>> wptr;
		std::size_t curr;  //当前下标
};

StrBlobPtr& StrBlobPtr::incr()
{
	check(curr, "increment past end of StrBlobPtr");
	++curr; //推进当前位置。  
	return *this;//为什么要return *this, 如果再次自加可以重复,举个例子就像赋值一样 a = b = c;  
	//如果不返回对象不能继续赋值。
} //return *this是一份拷贝。 return this是地址。

std::string& StrBlobPtr::deref()const
{
	auto p = check(curr, "dereference past end"); //shared_ptr引用计数会增加,但是作用域结束后,引用计数又会减1
	return (*p)[curr];//p是所指的vector,(*p)指向vector第一个元素,与int (*p)[10]一个道理
}

//check检查是否存在shared_ptr和大小
std::shared_ptr<std::vector<std::string>> StrBlobPtr::check(std::size_t i, const std::string &msg)const
{
	auto ret = wptr.lock(); //检查是否存在,存在返回shared_ptr,不存在返回空的shared_ptr.
	if (!ret)
		throw std::runtime_error("unbound StrBlobPtr");
	if (i >= ret->size())
		throw std::out_of_range(msg);
	return ret;
}


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

int main(int argc, char*argv[])
{
	std::fstream is(argv[1]);
	std::string s;
	StrBlob S;
	while (std::getline(is, s))
	{
		std::string temp;
		std::istringstream ist(s);
		while (!ist.eof())
		{
			ist >> temp;
			S.push_back(temp);
		}
	}

	std::cout << "size:" << S.size() << std::endl;

	StrBlobPtr sp(S);
	for (auto it =0 ;it!= S.size(); ++it)
	{
		std::cout << sp.deref() << std::endl;
		sp.incr();
	}
}


12.2动态数组

new和delete一次只能分配和释放一个对象,但有时我们需要一次为很多对象分配内存的功能

C++和标准库引入了两种方法,另一种new 和 allocator。

使用allocator 通常会提供更好的性能和更灵活的内存管理能力。

12.2.1new和数组

动态数组不是数组类型的。所以不能调用begin和end,也不能调用范围for语句。而是用数组维度来返回指向首元素和尾后元素的指针。


#include <iostream>  
#include <memory>  
  
using namespace std;  
  
typedef int arr[10];  //声明arr为一个int的10元素数组
  
int main()  
{  
    int *p = new int[10];  
    int *p2 = new arr;  //两个等价,返回指向第一个int的指针
    for(int i = 0; i < 10; i++)  
    {  
        p[i] = i;  
    }  
    for(int i = 0; i < 10; i++)  
        cout << p[i] << " ";  
    cout << endl;  
    //for(const int i : p);         //error:动态分配数组返回的不是数组类型,而是数组元素的指针。所以不能用范围for  
      
/*---------------------------------- */  
    //初始化动态数组  
    int *pi = new int[10];          //默认初始化
    int *pi2 = new int[10]();       //初始化为0,且有括号必须为空 +括号即可 
    string *ps = new string[10];       //10个空string  
    string *ps2 = new string[10]();    //10个空string  
    //可以使用列表初始化  
    int *pi3 = new int[10]{1,2,3,4,5,6,7,8,9,0};//初始值列表里的值不能多于容量,否则new失败,不会分配内存  
    string *ps3 = new string[10]{"a","b","c","d","e","f","g","h","i",string(3,'x')};  
    //释放动态数组  
    delete []pi3; //必须要加[]括号,且释放动态数字时是逆序释放。如果delete动态数组不加[],行为是未定义的。  
  
/*----------------------------------- */  
    //智能指针和动态数组,标准库定义了特别的unique_ptr来管理,当uo销毁它管理的指针时,会自动调用delete [];  
    int *p5 = new int[10];  
    //unique_ptr<int[]> up;     
    unique_ptr<int[]> up(p5);    
    for(int i = 0; i < 10; ++i)  
        cout << up[i] << " ";  
    cout << endl;  
    //如果使用shared_ptr的话我们必须自己定义delete函数  
    shared_ptr<int>sp(new int[10], [](int *p) { delete []p;});  
    //智能指针不支持算数类型,如果要访问数组中的元素,必须使用get函数返回一个内置指针。  
    cout << (*sp.get()) << endl;  
  
}  


12.2.2allocator类

引入allocator的原因是new类上的缺陷。我们分配单个对象时,希望将内存分配和对象初始化组合在一起。但是分配大块内存时,只有在需要时才真正执行对象创建操作。

new它将内存分配和对象构造结合到了一起,delete将对象析构和内存释放组合一起。就造成了不必要的浪费。

比如:string *p = new string;

new是现在找一块内存分配,不够继续malloc,在分配内存的地址上调用构造函数,delete也一样,在释放内存的时候也会调用析构函数。

内置类型要指定初值。

但是如果我们希望指定它的初值,不让它调用默认构造函数new就不可行了,而且本身调用了一次构造函数,然后我们赋值了一次。

更重要的是,没有默认构造函数的就不能动态分配内存了。


allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它分配的内存是原始的、未构造的。
 allocator<string> alloc;  
 auto const p = alloc.allocate(ivec.size()*4);//返回迭代器,指向第一个内存的位置。 


allocator<T> a 
a.allocate(n)       //分配一段原始的未分配的内存,返回迭代器,指向第一个内存位置
a.deallocate(p,n)   //释放指针p开始的内存
a.construct(p,args) //在p指向的内存中构造一个对象
a.destroy(p)        //对p指向的对象指向析构函数


标准库还为allocator定义了两个伴随算法

在未初始化的内存中创建对象,都定义在头文件memory

uninitialized_copy(b,e,b2)      //b,2是输入容器的迭代器,b2是内存的起始地址,要保证空间足够  
uninitialized_copy_n(b,n,b2)    //b是输入容器的起始迭代器,复制n个,复制到以b2为起始地址的动态内存中  
uninitialized_fill(b,e,t)       //b,e是动态内存的起始和终止位置,t是要fill的元素  
uninitialized_fill_n(b,n,t)     //b是动态内存的起始,fill n个,t是要fill的元素 




                
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值