(十二)动态内存

1对象生存期及内存分配

1.1对象生存期

  • 目前使用的对象有着严格定义的生存期。

全局对象:程序启动时分配,结束时销毁
局部自动对象:进入定义所在程序块定义,结束程序块销毁
局部static对象:第一次使用时分配,程序结束时销毁

  • c++还支持动态分配对象:生存期与创建位置无关,当显式被释放时,对象才会被销毁。为了保证动态对象能够正确被释放,标准库定义了两个智能指针类型管理动态分配的对象。当对象应该被释放时,指向它的智能指针能确保自动释放它。

1.2对象内存分配

1.2.1静态内存

  • 保存局部static对象、类static数据成员和定义在任何函数之外的变量
  • 由编译器自动创建和销毁

1.2.2栈内存

  • 保存定义在函数内的非static对象。
  • 仅在定义程序块运行时才存在。

1.2.3堆内存(内存池/自由空间)

  • 程序运行时分配的对象,生存期由程序来控制,需要显式销毁。

2动态内存与智能指针

2.1 动态内存管理

2.1.1 进行动态内存管理的原因

1.程序不知道要使用多少对象:例如,容器类
2.程序不知道所需对象的准确类型
3.程序需要在多个对象间共享数据:例如,智能指针

2.1.2 直接管理动态内存

  • 动态内存管理通过两个运算符完成:

new:在动态内存中为对象分配空间并返回一个指向该对象的指针,可以选择对对象初始化。
delete:接受一个动态对象指针,销毁对象并释放内存空间。

  • 自己管理内存的类不能依赖类对象拷贝、赋值和销毁操作的任何默认定义。
2.1.2.1 运算符new分配内存
  • 运算符new进行分配内存
	int* p1 = new int; //指向一个未初始化的int
	int* p2 = new int(42); //值初始化
	std::string* p3 = new std::string(10, 'h'); //构造初始化
	int* p4 = new int(); //默认初始化
	auto* p5 = new int(42);//提供括号包围的初始化器时可以使用auto
//对于const类型对象
	const int* p6 = new const int(1024); //返回指向const int的指针
	const int* p7 = new const int; 
  • 当堆空间被耗尽时,new表达式会失败。此时会抛出bad_alloc的异常。如果不想抛出异常,可以改变new使用方式。 bad_alloc和no_throw都在头文件new中。
int *p1 = new int;	//分配失败时抛出bad_alloc的异常
int *p2 = new (std::nothrow) int; //定位new,分配失败时返回空指针
  • 为防止内存耗尽,通过运算符delete将动态空间归还给系统。
2.1.2.2 运算符delete归还内存
  • 传递给delete的指针必须是动态分配的内存,或者是空指针。释放非堆内存,或对相同指针释放多次行为是为定义的。编译器无法分辨指针指向的区域是什么内存,以及该内存是否被释放。
	int* p9 = new int(42); //指向一个未初始化的int
	delete p9;
  • const类型动态对象可以被删除。
  • 由于通过内置指针类型来管理的动态对象,在显式被释放之前他是一直存在的。所以对于函数内使用的指向动态内存的指针(不是智能指针,智能指针可以自己释放),需要显式释放内存。否则存在指针销毁,但函数结束,指针销毁,而内存无法释放的情况。
int* process(int i) {
	int*p = new int(i);
	return p;
}
void process2(int* p) {
	int* p1 = p;  //不再使用该内存空间时,应delete掉,否则离开该函数后,内存将无法被释放掉
	delete p1;
}
int main() {
	process2(process(1));
	return (0);
}
2.1.2.3 使用运算符new和delete时注意事项
  1. 忘记delete内存。会产生内存泄漏,内存无法归还给自由空间;
  2. 使用已经释放掉的对象。一方面,当指针delete后,指针变成了空悬指针,指向一块曾经保存数据对象但现在已经无效的内存指针,建议释放内存后将指针值为空。另一方面可能有多个指针指向相同的内存,很难查找重置指向该内存的其他指针;
  3. 同一块内存释放两次,两个指针指向相同动态分配空间时,两个指针都进行了delete,会导致自由空间的破坏。

综上,相比内置指针管理的动态空间,使用智能指针可以有效避免以上问题。

2.2智能指针概述

  • 用来管理动态对象。行为类似常规指针,区别是可以自动释放指向的对象。

shared_ptr:允许多个指针指向同一对象
unique_ptr:“独占”所指向的对象
weak_ptr:伴随类,是一种弱引用,指向shared_ptr所管理的对象。

  • 智能指针头文件:

#include<memory>

2.3shared_ptr类

2.3.1shared_ptr类初始化

  • 初始化shared_ptr类,需要给出指针指向的类型。
  • 可以采用make_shared标准库函数。在动态内存中分配一个对象并初始化它,返回此对象的shared_ptr类型。定义在头文件memory.h中。使用make_shared函数,需要给出函数创建对象的类型,并通过对应类型的构造函数初始化相应值。当传参为空时为值初始化。
  • 一般初始化方式
	shared_ptr<int> p1; //空指针
	shared_ptr<int> p2 = make_shared<int>(42); //采用make_shared函数,创建一个指向int类型,值为42的智能指针
	shared_ptr<int> p3 = make_shared<int>(42); //采用make_shared函数,采用值初始化
	auto p5 = make_shared<int>(); //采用make_shared函数,采用值初始化
  • 对于使用指针进行智能指针初始化的情况:接受指针的智能指针构造函数是explicit的,必须值初始化,不能拷贝初始化。指针必须指向动态内存,除非重载delete函数。
	shared_ptr<int> p4(new int(42));//使用new返回的指针初始化,函数返回时也要绑定后再返回
	shared_ptr<int> p6(p1);
	unique_ptr<int> u1;
	shared_ptr<int> p7(u1); //从u处接管所有权,将u置空
	shared_ptr<int> p6(p1,del); //接管q所有权,使用可调用对象d代替函数delete
	p.reset((q),(del)); //p指向q/空,使用可调用对象d代替函数delete	

2.3.2shared_ptr类拷贝赋值

  • 每个shared_ptr都有一个关联的计数器,记录有多少个其他shared_ptr指向相同对象。当进行拷贝时,计数器会递增。当shared_ptr赋新值或被销毁时,计数器会递减。
  • 当shared_ptr计数器变为0时,他会自动释放自己所管理的对象。shared_ptr类的销毁是通过析构函数完成的,析构函数用来释放对象所分配的资源,控制该类对象销毁时的操作。
	shared_ptr<int> p3 = make_shared<int>(); //采用make_shared函数,采用值初始化
	auto p5(p3);
	cout << p5.use_count() << endl; //2
	p5 = p1; //
	p3 = p1;//此时p3原来指向的对象已没有引用者,会自动释放
  • 对于分配的内存,只要有shared_ptr引用该内存,则该位置内存不会被释放掉。
shared_ptr<int> process(int i) {
	shared_ptr<int> p = make_shared<int>(i);
	cout << p.use_count() << endl; //1
	return p; //返回后p被销毁,但拷贝给了外部的值,内存不会释放
}
int main(){
	auto p6 = process(1);  
	cout << p6.use_count() << endl;  //1
	return 0;
}
  • 一种忘记销毁shared_ptr类的情况:将shared_ptr类存于一个容器中,然后重排了容器,从而不需要一些元素,此时应该用erase删除不需要元素,防止内存浪费。

2.3.3shared_ptr类操作

//shared_ptr和unique_ptr都支持的操作:
shared_ptr<T> sp;   //空智能指针
unique_ptr<T> up;
p  //条件判断,不为空时返回true
*p //解引用 
p->mem
p.get() //p中保存的指针,注意销毁后不能用了
swap(p,q) //交换
p.swap(q)

//shared_ptr独有操作
make_shared<T>(args) 
shared_ptr<T>p(q)//拷贝
p = q //赋值
p.unique() //多个还是1个智能指针指向p,1个智能指针时返回true
p.use_count()//指向p的智能指针数目
  • 对于get函数:当某些代码块不能使用智能指针,必须使用内置指针时使用get函数。需要注意转换的内置指针不会被销毁,否则内置指针的内存会被释放,导致get()前的指针变为空指针。不要使用get初始化或赋值另一个智能指针,以免出现多套智能指针公用一个内存空间的情况。

2.3.4shared_ptr类应用

  • 通过shared_ptr让多个类对象不同拷贝之间共享数据。
  • 实现类说明: 设计一个管理多个string的类,多个类对象可以共享相同的数据。类操作包括修改访问元素,试图访问不存在元素时,抛出一个异常。

考虑要管理多个string,且多个类对象共享相同数据。数据使用vector类型保存,内存为动态内存。
使用vector容器中函数进行相关操作
类的拷贝赋值销毁函数采用默认版本。

#include<iostream>
#include<string>
#include<vector>
#include<memory>
#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(); }
	//增删元素
	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;
};
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);
	}
}
std::string& StrBlob::front() {
	check(0, "front on empty StrBlob");
	return data->front();
}
std::string& StrBlob::back() {
	check(0, "back on empty StrBlob");
	return data->back();
}
void StrBlob::pop_back() {
	check(0, "pop_back on empty StrBlob");
	return data->pop_back();
}

int main() {
	StrBlob strb1;
	strb1.push_back("1");
	auto strb2(strb1);
	strb2.push_back("2");
	std::cout << strb1.front() << std::endl; //1
	std::cout << strb1.back() << std::endl;//2
	return 0;
}

2.3.5智能指针与异常

  • 如果抛出异常导致程序过早结束,智能指针可以自动释放内存,new的内置指针不会,导致内存泄漏。
  • 所以要么使用智能指针,要么加个析构函数,要么每次都显式释放内存。
  • 删除器:代替delete函数,完成对shared_ptr中保存的指针进行释放的操作。当智能指针管理的资源不是new分配的是,额外传给它一个删除器。
void end_connection(connection *p){disconnect(*p);} //disconnect为析构相关操作
int main(){
	connection c = connect(&d);
	shared_ptr<connection>p(&c,end_connection);//使用自己的函数作为删除函数
}

2.4 unique_ptr

  • 一个unique_ptr指向一个对象。unique_ptr被销毁,所指空间就被释放。

2.4.1 unique_ptr初始化

unique_ptr<int> p1;
unique_ptr<int> p2(new int(42));
unique_ptr<int> p3(p2.release());
unique_ptr<int> p4;
p4.reset(p3.release());

2.4.2 unique_ptr拷贝

  • 一般情况下,unique_ptr不支持拷贝和赋值。
  • 一个例外条件是可以拷贝或赋值一个将要被销毁的unique_ptr。
//eg:从函数中返回一个unique_ptr
unique_ptr<int> process(int p){
	unique_ptr<int> up(new int(p));
	//...
	return up;
}

2.4.3 unique_ptr操作

	shared_ptr<T> sp;   //空智能指针
	unique_ptr<T> up;
	p  //条件判断,不为空时返回true
	*p //解引用 
	p->mem
	p.get() //p中保存的指针,注意销毁后不能用了
	swap(p,q) //交换
	p.swap(q)
	
	//unique_ptr独有操作
	unique_ptr<int> p1; //空指针
	unique_ptr<int> p4(new int(42));
	unique_ptr<T,D>u2 //用一个类型为D的可调用对象来释放它的指针
	unique_ptr<T,D>u2(d) //用一个类型为D的可调用对象d代替delete来释放它的指针
	u = nullptr //释放u指向的对象,将u置为空
	u.release() //放弃对指针的控制权,返回指针,将u置空
	u.reset()
	u.reset(q)  //内置指针
	u.reset(nullptr)
  • 对于release:如果不将指针值赋给另一个智能指针,release位置的内存不会被释放

2.4.3 unique_ptr删除器

  • 默认情况下用delete释放它指向的对象
  • 重载unique_ptr中默认的删除器会影响unique_ptr的类型以及如何构造该类型的对象。需要提供删除器类型。
void end_connection(connection *p){disconnect(*p);} //disconnect为析构相关操作
void f(destination &d){
	connection c = connect(&d);
	unique_ptr<connection,decltype(end_connection)*>p(&c,end_connection);
}

2.5 weak_ptr

  • weak_ptr指向shared_ptr对象,当shared_ptr被销毁时,不论weak_ptr是否指向对象,该对象动态空间会被释放。

2.5.1 weak_ptr操作

weak_ptr<T> w;
weak_ptr<T> w(sp);
w = p; //p为shared_ptr或weak_ptr
w.reset() //w置空
w.use_count() //与w共享shared_ptr数量
w.expired() //过期;失效的。w.use_count() 为0返回true
w.lock()//失效返回空shared_ptr,否则返回指向w对象的shared_ptr

2.5.1 weak_ptr初始化与调用

  • 要用shared_ptr进行初始化
  • 调用时使用lock函数,防止shared_ptr已经被销毁
	auto p = make_shared<int>(42);
	weak_ptr<int> wp(p);
	//调用时使用lock函数
	if (shared_ptr<int> np = wp.lock()) {//当lock不为空进入函数体
		//...
	}

2.5.1 weak_ptr应用

通过weak_ptr,可以防止类对象访问空成员对象

3动态数组

3.1 动态数组概述

  • 用于一次为很多对象分配内存的情况。动态数组不是数组类型,而是分配的内存空间,返回的是数组元素类型的指针。因此,不能调用begin、end和for循环等。
  • 有两种方法:

1.new表达式语法
2.allocator的类,允许将分配和初始化分离
分配动态数组的类必须定义自己版本的操作,包括拷贝、复制以及销毁对象管理所关联的内存。

3.2使用运算符分配及释放动态数组

3.2.1 使用new分配动态数组

int* pia = new int[10];
int* pia2 = new int[10]();
int* pia3 = new int[10]{ 0, 1, 2, 3, 4 }; //可以提供元素初始化器的花括号列表
//类型别名:typedef int arrT[42];
using arrT = int[42];
int* pia5 = new arrT;
  • 如果初始化器数目大于元素数目,会抛出bad_array_new_length的异常。定义在头文件new中。
  • 不能在括号中给出初始化器,所以不能用auto分配数组
  • 不能创建一个长度为0的数组,但可以在new时值为0:
	char arr[0]; //错误
	char* cp = new char[0]; //cp不能解引用,类似尾后指针

3.2.2 动态数组分配

	size_t n = 4;
	int* pia4 = new int[n];
	for (int* q = pia4; q != pia4 + n; ++q) {
		/*处理数组*/
	}

3.2.3 使用delete释放动态数组

  • 在指针前加一个方括号对。方括号对指示编译器此指针指向一个对象数组的第一个元素。数组中的元素按逆序销毁。
delete [] pia2;

3.3 智能指针与动态数组

  • 标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用一个unique_ptr管理动态数组,必须在对象类型后跟一对空方括号。
  • 当unique_ptr指向一个数组时,不能用点和箭头运算符(对一个数组用不了),可以使用下标运算符访问元素。
	unique_ptr<int[]>up(new int[10]);
	for (size_t i = 0; i < 10; ++i) {
		up[i] = i;
	}
	up.release(); //自动用delete[]销毁其指针
	//up.reset(); 为什么不是这个而是上边那个
  • shared_ptr不支持管理动态数组。如果需要使用,必须提供自定义删除器。shared_ptr不支持下标访问动态数组,要访问数组内元素需要用get获得一个内置指针在进行访问操作。
shared_ptr<int> sp(new int[10], [](int* p) {delete[]p; }); //shared_ptr类型为int,给的是首地址
for (int i = 0; i < 10; ++i) {
		*((sp.get()) + i) = i;
}
sp.reset(); //使用delete[]p释放动态数组

3.4 说明

  • 对于new:它将内存分配对象构造组合在了一起。
  • 对于delete:它将对象析构内存释放组合在了一起。
  • 在某些情况下,可能想将内存分配和对象构造分离或者为没有构造函数的类分配动态内存。此时可以使用allocator类。

4.3 allocator类

  • 头文件#include<memory>
  • allocator 分配的内存是原始未构造的。定义时要指明allocator可以分配的对象类型。

4.3.1 allocator操作

allocator<T> a;//创建对象

a.allocate(n)  //分配内存,分配一段原始未构造的内存,保存n个类型为T的对象
a.construct(p,args)//构造对象,p是类型为T*的指针,指向原始内存;arg为构造类型T对象的参数列表,在p指向内存中构造一个对象。
a.destroy(p)//析构对象,对p指向的对象执行析构
a.deallocate(p,n)//回收内存,释放从p开始的保存n个类型为T的对象的内存。p是由allocate返回的指针,n是p创建时大小。调用前,需要先进行destrory
	size_t n = 10;
	//1.定义对象
	allocator<string> alloc;
	//2.分配内存
	auto p = alloc.allocate(n); 
	auto q = p; //q用来指向末尾位置
	//3.构造对象
	alloc.construct(q++);  //
	alloc.construct(q++, 10, 'c');
	alloc.construct(q++, "hi");
	
	for (auto i = p; i != q; ++i) { //显示
		cout << *i << endl; 
	}
	//4.析构对象,析构只能对已经构造了的对象进行
	while (p != q) {
		alloc.destroy(--q);
	}
	//5.回收内存
	alloc.deallocate(p, n);
	return 0;

4.3.2 拷贝填充未初始化内存

  • 头文件#include<memory>
//4个函数都返回递增后目标迭代器位置
uninitialized_copy(b, e, b2); //将迭代器范围内元素给迭代器b2指定的未构造原始内存中
uninitialized_copy_n(b, n, b2);//将迭代器b开始那个元素给迭代器b2指定的未构造原始内存中
uninitialized_fill(b, r, t);//用值t创建迭代器范围内的元素
uninitialized_fill_n(b, n, t);//用n个值t创建迭代器b开始范围内的元素
	allocator<string> alloc;
	vector<string> vi = { "hhdy","zwhy","hh","xx" };
	 string* p1 = alloc.allocate(vi.size() * 2);
	 string* q1 = uninitialized_copy(vi.begin(), vi.end(), p1);
	q1 = uninitialized_fill_n(q1, vi.size(), "42");
	for (auto i = p1; i != q1; ++i) { //显示
		cout << *i << endl;
	}
	return 0;

在这里插入图片描述

5 应用

  • 文本查询程序
  • 说明:

实现单词查询,输出单词出现次数、所在行列表。行列表升序输出且不会重复输出。

  • 实现:

定义一个保存输入文件的类以及保存查询结果的类

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值