c++ primer / 第十二章 动态内存

本文深入探讨了C++中的内存分区,详细介绍了栈、堆、全局/静态存储区、常量存储区和代码区。此外,讲解了智能指针,包括shared_ptr、unique_ptr和weak_ptr的使用及注意事项,强调了它们在对象生命周期管理中的作用。还讨论了new和delete关键字的使用,以及动态数组的初始化。最后提到了避免空悬指针和智能指针与内置指针混用的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.内存分区

一般分为五个区
五个区存放
保存定义在函数内的非static对象,const 修饰的局部变量[声明周期结束由编译器自动销毁]
存储动态分配对象(new,malloc)[需要手动释放]
全局/静态存储区保存局部static对象,类static对象以及定义在任何函数之外的对象(全局) [程序结束编译器自动销毁]
常量存储区存放常量字符串,const 修饰的全局变量.
代码区存放函数体(类的成员函数、全局函数)的二进制代码;
int a = 0;
const int b = 0;

const char* c = "hello";
const char* d = "hello";

static int e = 0;
const static int f = 0;

cout << &a << endl;		//010FF96C			
cout << &b << endl;		//010FF960			//说明静态局部变量存储在栈中
cout << &c << endl;		//010FF954			//这是个误区,这样输出其实是输出指针的地址,指针存放在栈中
cout << static_cast<const void *>(c)<< endl;	//00612B3C		//这样才是输出字符串存放的地址
cout << static_cast<const void *>(d)<< endl;	//00612B3C		//说明指向同一个常量字符串的指针地址相同
cout << &e << endl;		//006192E4		
cout << &f << endl;		//00612B30		//说明const修饰的全局(静态)变量也是放在常量存储区

注意:

  • 指针本身存在调用栈中,指针指的数据在堆中。
  • 指向同一个常量字符串的指针地址相同

2.智能指针

1. shared_ptr

允许多个指针指向同一个对象,内部有一个引用计数.当引用指针为0时,会调用对象的析构函数释放空间

  • 递增
    • 当作为一个shared_ptr初始化另外一个shared_ptr
    • 作为参数传递给一个函数以及作为函数的返回值
  • 递减
    • 给一个shared_ptr附上一个新值
    • shared_ptr被销毁
    • 局部的shared_ptr离开作用域
//智能指针均为模板类
//使用*之前一定要初始化后,而且初始化值不能为空
shared_ptr<int> sptr/shared_ptr<int> sptr(nullptr);;
*sptr = 1;			//不可以,这时候sptr为nullptr,不能进行解引用 
cout << *sptr;

int a = 0;
shared_ptr<int> sptr(new int(a));
*sptr = 1;		//可行,因为sptr已经不为空了,解引用不会报错
cout << *sptr;

//如果是自带初始化的,就可以直接用*解引用
shared_ptr<string> sptr;
if (sptr && sptr->empty())
    *sptr = "hello";

//技术
智能指针 = 另一个智能指针 //左边计数-1,右边计数+1
2.函数
//1.make_shared:最安全的分配和使用动态内存的,可以用类型的构造函数
shared_ptr<int> sptr = make_shared<int>(42);
cout << *sptr << endl;
//也可以用auto
auto sptr = make_shared<int>(42);
cout << *sptr << endl;

//初始化和函数
shared_ptr<T> p(q)			//p管理内置指针q的对象,q必须是new分配的内存.且能转为T*型;
shared_ptr<T> p(u)			//p从unique_ptr那里抢过来对象的所有权,将u置空
shared_ptr<T> p(q,d)		//p接管了内置指针q所指向的对象的所有权.q必须能转为T*型.p将使用可调用对象d来代替delete;
shared_ptr<T> p(p2,d)		//p是p2的拷贝,区别在于p可调用对象d来代替delete
 
p.get();					//返回一个内置指针,指向智能指针管理对象.!少用!;如果被清理,智能指针也会变空悬指针
    
p.reset();					//reset释放p
p.reset(q);					//调用delete,释放完后指向q
p.reset(q,d);				//调用d释放p,再指向d
3.生存周期
  • 可以把他当初一个局部变量
  • 只有一种情况会不会释放,就是把指针放在容器中,但是结束的时候没有把他删除掉;用完记得erase()
2. unique_ptr

只允许当前指针独占对象,当unique_ptr被销毁,那么指向的对象也会被销毁

  • 没有make_shared函数类型的函数,只能通过new来完成绑定
  • 不支持普通的拷贝或者赋值
//直接初始化
unique_ptr<int> p2(new int(42));		//正确,直接初始化


//初始化和函数
unique_ptr<T> 	u1			//空指针,可以指向T*的对象
unique_ptr<T,D> u2			//u2可以调用D来释放它的对象
unique_ptr<T.D> u(d)		//空unique,指向类型为T*的对象,用类型为D的对象d代替delete
    
u = nullptr;				//直接释放对象,变为空指针
u.release();				//u放弃对指针的控制权,返回指针,将u置空, 指针被返回,可用于转移.
							//如果不是赋值智能指针,那么记得释放内存
    
p.reset();					//reset释放p
p.reset(nullptr);			//释放p,置空
p.reset(q);					//调用delete,释放完后指向q,可用于转移
传递和返回unique_ptr
//返回即将被销毁的智能指针
unique_ptr<int> clone(int p)
{
	return unique_ptr<int>(new int(p));		//正确
}

//返回局部对象智能指针
unique_ptr<int> clone(int p)
{
    unique_ptr<int> ret(new int(p))			//正确
	return ret;
}
3. weak_ptr

是一种软引用,可以指向shared_ptr 的对象.但是不会是shared_ptr的计数增加.

  • 因为计数不增加,所以shared_ptr被释放时,weak_ptr对象也被释放.所以在用时要先调用lock().
weak_ptr<T> w;				空weak_ptr可以指向T类型的对象
weak_ptr<T> w(sp);			与weak_ptr sp指向相同的weak_ptr,T必须能转换为sp指向的类型

w = p;						 p可以是一个shared_ptr或weak_ptr,运行后w和p指向共享对象

w.reset();					 将w置空
w.use_count();				 与w共享对象的shared_ptr的数量
w.expired();				 如果use_count()0,返回true,否则返回false
w.lock();					 如果w.expired()返回true,返回一个空的shared_ptr;
							 否则就返回一个指向w的对象的shared_ptr

3. new 关键字

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

//正常情况下,new失败就会抛出异常 bad_alloc,但是可以让他不抛出,返回null
int *p2 = new (nothrow)int;		//阻止抛出异常

4. delete 关键字

int i = 0;
int *pi1 = &i;
int *pi2 = nullptr;

double *pd = new double(33);
double *pd2 = pd;

delete pi1;			//编译通过,运行错误,pi1指向一个局部变量
delete pi2;			//正确,空指针可以delete


delete pd;			//正确,释放内存
delete pd2;			//编译通过,运行错误,未定义,内存以及被释放

//指针释放

空悬指针
double *pd = new double(33);
delete pd;			//正确,指针被释放后,会变成空悬指针,即有指向地址,但是地址已经被释放,无效了.再次delete就是未定义操作
pd = nullptr;		//如果还需要用,那么就指定为空;

//指向同一内容的指针
double *pd = new double(33);
auto pd2 = pd;
delete pd;		//pd2也会变为空悬指针
pd = nullptr;	//pd2为空指针,但是pd2仍未空悬指针
delete pd2;    	//报错

5. new 和 智能指针搭配

//可以让智能指针指向一块内存,但是想初始化必须是直接初始化
shared_ptr<int> p1 = new int(1024);		//错误,必须使用直接初始化形式.因为无法从内置指针转化为智能指针
shared_ptr<int> p2(new int(42));		//正确,直接初始化

不要混合使用内置和智能
void process(shared_ptr<int> ptr){...}


int *x(new int(1024));
process(x);		//错误,无法从内置指针转化为智能指针
process(shared_ptr<int>(x));		//正确,但是x的内容会被清除,变为空悬指针
*x;				//未定义

6.初始化动态数组

  • 在new一个非内置对象数组时,会在申请空间的基础上头部多申请4个字节用于存储数组长度,这样delete[]时候才知道对象数组的大小
int *pia = new int[10];		//10个未初始化的int,会调用类型的初始化.如stirng即未空
int *pia2 = new int[10]();	//10个0;

//释放
delete [] pia;

//内置对象可以不搭配使用.
int *pi2 = new int[2048]();
delete pi2;				
int *pi2 = new int(2048);
delete[] pi2;    //不会报错,编译器知道int是内置类型,不需要析构函数,所以也就不需要多4个字节来存放数组长度,只需要直接操作内存即可。


//使用非内置的对象是,动态数组一定要用[]
#include <iostream>
using namespace std;
class test {
public:
	test() { cout << "构造" << endl; }
	~test() { cout << "析构" << endl; }
};


//
int main(int argc, char *argv[]) {
	test *p = new test();
	delete[]p;			//程序不会当掉,但是会死循环打印析构.因为不知道需要析构多少次.
    
    test *p = new test[2];
	delete p;		//运行了1次析构后程序当掉.因为释放内存的时候只释放了起始地址为A的内存,然而这不是这一整块内存的起始地址,
    				//整块内存的起始地址应该是A-4,释放内存如果不从内存起始地址操作就会出现段错误,所以导致了程序挂掉。(长度不对,多了头部)
    
	return 0;
}

7.指向数组的unique_str,shared_ptr

//指向数组的unique_str不支持成员访问运算符(点和箭头),结束时自动调用delete[]
unique_str<T []> U;		//创建一个指向数组的unique_str
unique_str<T []> U(p);	//u指向内置指针p所指向的动态分配数组,p必须时T*类型
u[i];					//访问对象


//shared_ptr 释放时不会自己调用delete[],需要自己手动设置
shared_ptr<int> sp(new int[10],[](int *p){delete [] p;});
//shared_ptr不支持下标运算符.如果需要访问对象只能用get()返回一个内置指针,然后进行访问;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

公仔面i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值