C++笔记之内存(内存分区、动态内存、智能指针)

1.内存分区

1.1内存分区模型

内存分四区:代码区、全局区、栈区、堆区。前两者编译时划分,后两者运行时划分。全局区存放全局变量和静态变量以及常量,其中常量区里含有字符串常量和其他常量(const修饰的全局变量)且常量区的数据不可更改。栈区包含形参、局部变量等。堆区使用malloc、new等函数开辟的空间。(内存分区详情以及new的使用见《C++笔记二》第一节内存四区。)

示例:

const int t1 = 10;
int t2; 
void test21(int t21)
{
	
	cout<<"栈区——函数形参的地址:"<<&t21<<endl;
}
void test2()
{	
	int *a;		 
	cout<<sizeof(a)<<endl; 	//测试指针所占内存
	a = new int;
	cout<<"堆区——堆区开辟的地址:"<<a<<endl;
	delete a;
	a = NULL; 
	
	const	char *cPtr1 = "abcde";
	const	char *cPtr2 = "abcde";
	cout<<"全局区——字符串常量abcde的地址:"<<&"abcde"<<endl;
	cout<<"栈区——指针cPtr1的地址"<<&cPtr1<<endl;	
	cout<<"栈区——指针cPtr2的地址"<<&cPtr2<<endl;	
	
	int t;
	cout<<"栈区——局部变量的地址:"<<&t<<endl;
	test21(10);	//函数形参_栈区 
	
	cout<<"全局区——常量区的其他常量即const修饰的全局变量的地址:"<<&t1<<endl;
	cout<<"全局区——全局变量地址:"<<&t2<<endl;
	} 
//结果:
//8
//堆区——堆区开辟的地址:0xb31510
//全局区——字符串常量abcde的地址:0x488000
//栈区——指针cPtr1的地址0x6ffde0
//栈区——指针cPtr2的地址0x6ffdd8
//栈区——局部变量的地址:0x6ffdd4
//栈区——函数形参的地址:0x6ffdb0
//全局区——常量区的其他常量即const修饰的全局变量的地址:0x4880fc
//全局区——全局变量地址:0x4a8030

其一:指针占内存空间为8字节,这与操作系统的位数有关(见《C++笔记一》第七节指针)。指针存放的是地址,64位系统显然可以有64个“位置”给0、1来排列组合以表示地址,所以地址长度就是64比特也就是8字节。内存最大也就是2的64次方比特。

其二:指针cPtr1的地址与指针cPtr2的地址相差8,原因就是一个地址对应一个存储单元,一个存储单元可以存储1个字节的内容,而指针变量大小占8个字节(64位系统),所以相邻俩指针地址相差8。

2.动态内存

2.1动态内存基本概念

1> 动态分配的对象(运行时分配的对象,其生存周期由程序控制)存储在堆中
2> 动态内存:程序员根据需求在堆中开辟的一块内存。
3> 动态内存的管理由new\delete完成的。

2.2使用动态内存的原因

1> 程序不知道自己需要使用多少对象。(比如我们使用容器类,当空间不够时可以动态扩展。又比如看直播,有时10人,有时10000人,使用动态内存可以来一个人创建一个对象)
2> 程序不知道所需对象的准确类型。(因为有多态的存在。比如商品基类,子类为锅碗瓢盆等,有个商品上架的操作需要商品的指针,具体是什么商品不确定)
3> 程序需要在多个对象间共享数据。(为了更容易更安全的使用动态内存,C++里提供了指针指针,像shared_ptr可以让多个指针指向同一对象,实现共享。问题是static也可以实现共享,为什么不多用static?因为动态内存空间更大,使用更加灵活,可以由程序员自己创建、销毁。但static创建后就位于全局区,使用不够灵活,且占用内存太大)

2.3使用动态内存的几个问题

1> 确保在正确的时间释放动态内存是及其关键的,如果我们忘记释放,就会造成内存泄漏。
2> 有时在还有指针指向动态内存时,我们就释放了,这就会出现引用非法内存的指针的问题。

3.直接内存管理

使用new分配空间,使用delete释放空间。

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

1>默认初始化,内置类型的对象的值未定义,类类型则调用默认构造函数。
type * p = new type;
2>直接初始化,使用圆括号(或者列表初始化使用花括号),调用相应的构造函数
type * p = new type(value);
type * p = new type {v1,v2……};
vector< int > * p= new vector< int >{1,2,3,4,5};
3> 值初始化,类型名后跟空括号
type * p = new type();
注意:对于类类型来说默认初始化与值初始化效果完全一样,都是调用默认构造函数。对于内置类型来说使用默认初始化,其值未定义;使用值初始化,有良好定义的值。

4> 使用auto初始化
auto p1 = new auto(value);
注意,括号中只能有单一初始化器才能使用auto!!!auto *p = new auto(value1, value2);是错误的。

5> 允许动态分配const对象
分配const对象,返回的是常量指针。动态分配const对象,必须进行初始化,若没有默认构造函数,则只能显示初始化;若有默认构造函数可以隐式初始化。

void test()
{
    const int *p = new const int(1024);
    cout<< *p<<endl;
    delete p;
    
    const string *stp = new const string;
    delete stp;
}

3.2内存耗尽与释放动态内存

1> 内存耗尽,new表达式就会失效,抛出一个bad_alloc异常。同时也可以不让new抛异常,在new与数据类型之间加(nothrow),如int * p = new (nothrow) int;
2> 释放动态内存
使用delete释放动态内存,delete释放的必须是动态内存或者空指针。非new分配的内存或者相同的指针值释放多次是未定义的。释放const动态内存与一般的一样。delete之后记得将nullptr赋予指针,但是此种保护有限,倘若有两个指针同时指向一块动态内存,释放后,置空一个不影响另一个指针,所以要都置空,但在实际应用中找指向相同内存的指针还是很困难的。

3.3使用new\delete管理动态内存的三个常见问题

1> 忘记delete内存,造成内存泄漏。
2> 使用已经释放的对象,如果是置空了,还有可能发现这种错误(访问空指针,会报读写访问权限冲突的错误)。
3> 同一块内存释放两次。
解决办法:使用智能指针,这些都不是问题!!!

4.智能指针

为了更加容易更加安全的使用动态内存,C++提供了智能指针去管理动态对象,shared_ptr、unique_ptr、weak_ptr。智能指针都是类模板其中shared_ptr,允许多个指针指向同一个对象;unique_ptr只允许一个指针指向一个对象;weak_ptr指向shared_ptr所指对象,但不会影响shared_ptr指针的计数。智能指针的使用方式与普通指针类似,且这三种智能指针都在头文件memory中

4.1shared_ptr

shared_ptr支持的操作(《C++primer P401》):
C++笔记之内存(内存分区、动态内存、智能指针)_第1张图片C++笔记之内存(内存分区、动态内存、智能指针)_第2张图片C++笔记之内存(内存分区、动态内存、智能指针)_第3张图片

1> make_shared函数(shared_ptr的构造)
最安全的分配和使用动态内存的方法就是调用make_shared函数。

make_shared函数是在动态内存中分配一个对象并初始化它,返回一个指向该对象的shared_ptr。make_shared也在头文件memory中。在调用该函数时,make_shared< T >(参数);参数与类型T的构造函数匹配即可!而且可以通过auto来定义一个对象去保存shared_ptr。

class MyPrint
{
public:
	void operator()(int v)
	{
		cout << v << "\t";
	}
};

void test()
{
	shared_ptr<vector<int>> vp = make_shared<vector<int>>();
	for (int i = 0; i < 5; i++)
	{
		(*vp).push_back(i);
	}
	for_each((*vp).begin(), (*vp).end(), MyPrint());	//0 1 2 3 4
	cout << endl;

	shared_ptr<int> inp = make_shared<int>(10);
	cout << *inp << endl;

	auto sp = make_shared<string>("hello!");
	cout << *sp << endl;
}

2> shared_ptr的拷贝与赋值
每个shared_ptr都有一个关联的计数器,通常称为引用计数。执行拷贝或者赋值时,引用计数就会加1。

void test()
{
	shared_ptr<int> inp = make_shared<int>(12);
	cout << inp.use_count() << endl;	//1
	shared_ptr<int> inp1 = inp;	//赋值
	cout << inp.use_count() << endl;	//2
	auto inp2(inp);	//拷贝
	cout << inp.use_count() << endl;	//3
}

3> shared_ptr自动销毁所管理的对象
当最后一个shared_ptr被销毁时(计数为0),shared_ptr会调用析构函数,销毁对象,释放空间。

注意:当shared_ptr没有使用时一定要销毁,不然会浪费内存。特别是当shared_ptr存储在容器中时,如果不再使用,记得及时删除那些不需要的元素。

4>shared_ptr与new结合起来使用(表12.3)
第一:指针指针的构造函数是explicit的(《C++primer》P265),所以不支持隐式转换。所以内置指针无法隐式转换成智能指针

void test()
{
	//shared_ptr p1 = new int(10);	//错误
	shared_ptr<int> p2(new int(20));	//正确
	int * p = new int(30);
	shared_ptr<int> p3(p);	//不推荐智能指针与内置指针混合使用
	cout << "p2:" << *p2 << "     p3:" << *p3 << endl;
}

所以,当函数返回类型是智能指针时,return new int(value); 也是有问题的!!!必须转换成return shared_ptr< int >(new int(value));

第二:一个用于初始化智能指针的内置指针必须是指向动态内存的,简单说内置指针必须是通过new得到的。因为智能指针默认通过delete释放内存。(虽然智能指针并不是一定指向动态内存,如表12.3中第三第四两个方法,但是这里使用内置指针初始化,如表12.3中的第一个,没有删除器,然而智能指针默认的是进行delete,所以只能是通过new获得的内置指针)

第三:不要混合使用智能指针与内置指针
shared_ptr可以协调对象的析构,这仅限于其指针的拷贝。所以,最好使用make_shared,避免了new,也避免了既出现智能指针又出现内置指针的情况(如果智能指针销毁了,内存被释放,内置指针依然指向被释放的内存;如果内置指针delete了,智能指针只要销毁就会出错,销毁时又释放一次内存),还避免了出现多个指向同一块内存但相互独立的智能指针(shared_ptr可以协调析构仅仅是发生了拷贝,倘如因为new产生内置指针,然而又想让多个智能指针指向这块内存,便用new产生的内置指针直接初始化多个智能指针,虽然这些智能指针指向同一块内存,但相互独立,析构互不影响)。
错误示例:

//错误示例
void test()
{
	int * p = new int(10);
	shared_ptr<int> p1(p);
	shared_ptr<int> p2(p);	
}

第四:不要用get去初始化另一个智能指针或者为智能指针赋值
使用get返回一个指向智能指针管理的对象的内置指针,目的是为了给那些不能使用智能指针的代码传递一个内置指针。而且还要确保该内置指针不被delete掉。显然不能通过get得到的内置指针去给智能指针赋值,因为内置指针无法隐式转换成智能指针。但也不能去初始化一个另一个智能指针,因为效果与上面不要混合使用智能指针与内置指针讨论的一样。简单说,此用法比较特殊,少用。

第五:其他shared_ptr操作(reset)
reset可以将新的内置指针赋予一个shared_ptr(与赋值类似,所以还是会出现多个相同指向但又相互独立的智能指针的情况),通常可以与unique()一起使用,修改智能指针的指向。

5>智能指针的一个用法(可能不够完善,等后期实际遇到了再做补充)
在确保发生异常后资源能够正确的释放,可以使用智能指针。比如:某个函数由于发生异常过早结束,如果使用了智能指针,由于是局部变量,当函数结束时,智能指针也会被销毁,然后就会检测引用计数,来判断是否释放资源。当然这里不一定是动态内存,但如果是其他资源,需要传递一个删除器,像表12.3中第三第四个方法以及最后一个reset。删除器就是为那些不是通过new获取的资源准备的,而智能指针的作用就是确保资源能够更加安全、方便的使用。

6>智能指针的陷阱
1> 不要用相同的内置指针去初始化多个智能指针
2> 不delete通过get获得的内置指针
3> 不使用get去初始化或者reset另一个智能指针
4> 如果使用了get,需要记住当最后一个智能指针销毁时,该指针就无效了
5> 如果智能指针管理的资源不是通过new获取的内存,那么需要传个删除器进去

4.2unique_ptr

某个时刻只能有一个unique_ptr指向给定对象,一旦unique_ptr销毁,所指对象就被销毁。unique_ptr的一些操作(《C++primer》P418),与shared_ptr相同的操作见上面的表12.1
C++笔记之内存(内存分区、动态内存、智能指针)_第4张图片

1> 初始化
定义unique_ptr时需要绑定一个new返回的指针,而且也只能直接初始化。区别是unique_ptr不支持拷贝与赋值操作

2> unique_ptr不允许拷贝与赋值但有特例
虽然不能够拷贝与赋值,但是可以通过release与reset将指针的所有权从一个非cosnt的unique_ptr转移到另一个unique_ptr。
而且不能拷贝unique_ptr有特例:我们可以拷贝或者赋值一个将要被销毁的unique_ptr。比如从函数返回一个unique_ptr。

3> 向unique_ptr传递删除器
与shared_ptr一样,智能指针被销毁时会默认使用delete释放对象。而且unique_ptr也可以传递一个删除器,unique_ptr传递删除器需要在尖括号中给出删除器类型。

4.3weak_ptr

weak_ptr是一种不控制所指对象生存期的智能指针,它指向shared_ptr所指对象,但不增加shared_ptr的引用计数。而且,最后一个shared_ptr被销毁时,不管有没有weak_ptr指向该对象,该对象都会被销毁。所以,weak_ptr的目的就是既想“了解”所指资源的信息又不像与其有所牵连。
weak_ptr的一些操作见《C++primer》P420
C++笔记之内存(内存分区、动态内存、智能指针)_第5张图片1> 创建weak_ptr
创建weak_ptr需要使用shared_ptr进行初始化。由于对象可能不存在,所以不能直接使用weak_ptr访问对象,要用lock获取是否有指向对象的shared_ptr。

void test()
{
	shared_ptr<int> p = make_shared<int>(10);
	weak_ptr<int> wp(p);
	if (wp.use_count()!=0)
	{
		cout << *(wp.lock()) << endl;	//返回10
	}
}

2> 示例:

class A
{
public:
	shared_ptr<A> m_sp;
};

void test()
{
	A a1;
	A a2;
	//sp1指向a1,a1的成员变量指向a2;
	//sp2指向a2,a2的成员变量指向a1;
	shared_ptr<A> sp1 = make_shared<A>(a1);
	shared_ptr<A> sp2 = make_shared<A>(a2);
	a1.m_sp = make_shared<A>(a2);
	a2.m_sp = make_shared<A>(a1);
	//这样构成循环,可以通过weak_ptr解除循环
	weak_ptr<A> wp(sp1);
	if (wp.use_count() == 1)
	{
		wp.lock()->m_sp.reset();	//将sp1所指对象a1的成员变量置空
	}
	//循环解除
}

5.动态数组

虽然使用new、delete可以一次分配、释放一个对象,但有时需要一次给很多对象分配内存的功能,这里就要用动态数组。C++可以使用new来分配动态数组,但还可以使用allocator类u,该类可以把分配与初始化分离,而且拥有更好的性能与更加灵活的内存管理能力。

5.1new与数组

1> 定义动态数组与释放动态数组
分配数组可以得到一个元素类型的而且指向首元素的指针。因为得到的是指针,所以动态数组不是数组类型,显然无法使用begin()与end()!!!

void test()
{
	int *a = new int [5] {1, 2, 3, 4, 5};
	string *as = new string[5];
	delete[]a;
	delete[]as;
}

注意:必须给出数组大小,倘若初始化器数目大于元素数目,则new失败,抛出bad_array_new_length异常。动态分配空数组也是合法的!

初始化动态数组与释放动态数组示例如上,只是多了对中括号。而且和上面的一样也有值初始化([ ]后加个空括号)。

int *a = new int[10];	//十个元素都没有初始化
int *b = new int[10]();	//十个元素都初始化为0

2>动态数组与智能指针
C++提供了一个可以管理new分配的动态数组的unique_ptr。同上只是加了个中括号。注意:shared_ptr不能直接管理动态数组,如果想用,那就要自己定义删除器。不然,只delete第一个元素,就像delete没加[ ]的效果一样。同时shared_ptr没有重载[ ],所以只能通过get获取内置指针,再去操作。

void test()
{
	unique_ptr<int[]> ap(new int[10]{ 1,2,3,4,5,6,7,8,9,0 });
	for (int i = 0; i < 10; i++)
	{
		cout << ap[i] << endl;
	}
	ap = nullptr;
}

指向数组的unique_ptr的一些操作《C++primer》P426
C++笔记之内存(内存分区、动态内存、智能指针)_第6张图片

5.2allocator类

1>使用new\delete的弊端
new、delete把内存分配与对象构造、对象析构与内存释放组合在一起了,所以有时new的时候会分配很大块内存,却用的不多,可能导致不必要的浪费,有局限性。而且更重要的是,那些没有默认构造函数的类不能动态分配数组。

class A
{
public:
	A(int v) :m_A(v) {}
	int m_A;
};

void test()
{
	A *p = new A[10];	//报错:A没有默认构造函数
	int *b = new int[100]{1,2,3,4,5};
}

2>allocator
allocator是类模板,在头文件memory中。allocator可以将内存分配与对象构造分开。
allocator类及其算法《C++ primer》P428
C++笔记之内存(内存分区、动态内存、智能指针)_第7张图片allocator< T > a; //确定类型
a.allocate(n); //分配内存,是未构造的内存,返回类型为T的指针
a.construct(p, args) //p是类型T的指针,args是构造函数,构造1个对象
a.destroy§; //析构p所指对象
a. deallocate(p,n); //释放从p所指空间开始的n个对象,而且一定要保证释放空间前,对象都析构了(不管是类类型还是内置类型)。p必须是allocate返回的指针,n必须是allocate里的n。

class A
{
public:
	A(int v) :m_A(v) {}
	int m_A;
};

void test()
{
	allocator<A> a;	//A 类型
	auto p = a.allocate(5);	//5个A对象
	auto p1 = p;
	for (int i = 1; i < 6; i++)
	{
		a.construct(p1++, A(i));	//构造对象
	}
	cout << (p+2)->m_A << endl;	//返回3
	for (int i = 1; i < 6; i++)
	{
		a.destroy(--p1);	//析构对象,这里p1最开始指向最后一个元素的后一位
	}
	a.deallocate(p, 5);	//释放内存
}

allocator还有两个伴随算法,在头文件memor中。《C++ primer》P429
C++笔记之内存(内存分区、动态内存、智能指针)_第8张图片举个例子:我们像把int型vector容器存储到动态内存里,内存是之前两倍,拷贝放到前半部分,后半部分用给定值填充。

void test()
{
	vector<int> v;
	for (int i = 0; i < 10; i++)
	{
		v.push_back(i);
	}
	allocator<int> a;
	auto p = a.allocate(2 * (v.size()));	//申请内存
	auto p1 = p;
	auto p2 = uninitialized_copy(v.begin(), v.end(), p1);	//p2指向第11个元素
	uninitialized_fill_n(p2, 10, 6);
	for (int i = 0; i < 20; i++)
	{
		cout << *(p++) << "  ";
	}
	//0  1  2  3  4  5  6  7  8  9  6  6  6  6  6  6  6  6  6  6
	cout << endl;
	auto pl = p;
	for (int i = 0; i < 20; i++)
	{
		a.destroy(--p);
	}
	a.deallocate(p, 2 * (v.size()));
}

你可能感兴趣的:(C++)