第3章 内存管理
3.1 程序内存布局
C++程序 编译为二进制后,运行时载入内存。
运行时内存分布从低地址到高地址,分别为 代码段-初始化数据段-未初始化数据段-映射段-栈-内核空间。
程序中使用了内存映射文件,如共享库、共享文件,则包含映射段。
段名 | 存储内容 | 分配方式 | 生长方向 | 读/写特点 | 运行态 |
代码段(text segment) | 程序指令、字符串常量、虚函数表 | 静态分配 | 由低到高 | 只读 | 用户态 |
初始化数据段(data) | 初始化的全局变量和静态变量 | 静态分配 | 由低到高 | 可读可写 | 用户态 |
BSS段 | 未初始化的全局变量和静态变量 | 静态分配 | 由低到高 | 可读可写 | 用户态 |
堆(heap) | 动态申请数据 | 动态分配 | 由低到高 | 可读可写 | 用户态 |
映射段 | 动态链接库、共享文件、匿名映射对象 | 动态分配 | 由高到低 | 可读可写 | 用户态 |
栈(stack) | 局部变量、函数参数与返回值、函数返回地址、调用者环境信息 | 静态+动态分配 | 由高到低 | 可读可写 | 用户态 |
内核空间(kernel space) | 操作系统、驱动程序 | 动态+静态 | 由低到高(内核的数据段)+由高到低(内核栈) | 不能直接访问 | 内核态 |
3.2 堆与栈的区别
1) 在程序内存布局场景下,堆与栈表示的是两种内存管理方式
2)在数据结构场景下,堆与栈表示两种常用的数据结构
3.2.1 程序内存分区中的堆和栈
1、栈(由操作系统自动分配释放)
存放函数的参数值、局部变量等,操作方式后入先出,内存生长方向由高到低。后定义的变量地址低于先定义的变量。
栈中存储的数据生命周期随函数的执行完成而结束。
2、堆(由程序员分配释放),程序员不释放,则程序结束时由系统回收,分配方式类似于链表
地址由低到高,注意后申请的内存空间不一定在先申请的内存空间的后面(先申请的可能释放),管理方式:操作系统内部有一个记录空闲内存地址的链表
//C中
char *p1 = (char*)malloc(10);
free(p1);
//C++中
char p2 = new char[10];
delete []p2;
3、堆和栈的区别
堆和栈实际上是操作系统对进程占用的内存空间的两种管理方式,主要有如下几种区别。
栈 | 堆 | |
管理方式 | 由操作系统自动分配释放,无需手动控制 | 申请和释放均由程序员控制,容易产生内存泄漏 |
空间大小 | 每个进程拥有的栈的大小要远远小于堆的大小 | |
生长方向 | 栈的生长方向向下,内存地址由高到低 | 堆的生长方向向上,由低到高 |
分配方式 | 栈由2中动态分配和静态分配,静态分配(操作系统完成,比如局部变量的分配)动态分配由alloca函数进行分配,栈的动态分配由操作系统进行释放,无需手工实现 | 堆是动态分配,没有静态分配的堆 |
分配效率 | 栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,栈的效率较高 | 堆是由C/C++库函数或运算符来申请与管理的,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然堆的效率要低很多 |
存放内容 | 栈存放内容:返回地址、相关参数、局部变量和寄存器内容等,调用函数时对当前函数执行断点进行保存,需要使用栈来实现。 | 堆中存放内容由程序员来实现 |
3.2.2 数据结构中的堆和栈
1、栈简介(先进后出)
一种运算受限的线性表,仅允许在表的一端(栈顶Top)进行插入和删除操作,另一端称为栈底bottom。
可以用数组和链表实现(单向链表、双向链表或循环链表)作为底层数据结构
栈的基本操作包括初始化、判断栈是否为空,入栈、出栈以及获取栈顶元素。
#include <stdio.h>
#include <malloc.h>
#define DataType int
#define MAXSIZE 1024
struct SeqStack
{
DataType data[MAXSIZE];
int top;
};
//栈初始化,成功返回栈对象指针,失败返回空指针
SeqStack* initSeqStack()
{
}
//判断栈是否为空
bool isEmptySeqStack(SeqStack *s)
{
}
//入栈
int pushSeqStack(SeqStack *s, DataType x)
{
}
//出栈
int popSeqStack(SeqStack* s, DataType * x)
{
}
//取栈顶元素
int topSeqStack(SeqStack* s, DataType* x)
{
}
//打印栈中元素
int printSeqStack(SeqStack* s)
{
}
//test
int main()
{
}
2、堆简介
(1)堆的性质
堆是一种常见的树形结构,是一种特殊的完全二叉树,并且仅当满足所有节点的值总是不大于或小于其父节点的值得完全二叉树被称为堆。堆的这一特性称为堆序性。
(2)堆的基本操作
1)建立
2)插入
3)删除
(3)堆操作的实现
(4)堆的具体应用--堆排序
3.3 new的3种面貌
3.4 delete的3种面貌
3.5 new和delete的使用规范
1. new和delete需一一对应
用new操作申请空间,如果申请成功,就必须在以后的某个时刻用delete释放该空间,既不能忘记释放,也不能多次释放。
前者会引起内存泄漏,后者会引起运行时错误。
2. new[]和delete[]需一一对应
申请对象数组时,需要使用new[ ]运算符,与之对应,释放对象数组时,需要使用delete[ ]运算符。
注意以下2点:
1)对于内置数据类型,因为没有构造函数和析构函数,所以使用delete和delete[ ]的效果是一样的。
int *pDArr = new int[3];
//processing code
delete pDArr; //等同于delete[] pDArr
对于内置类型,还是建议使用delete[ ] 与new[ ]一一对应,保证代码的可读性
2)对于经常适应typedef的程序员来说,很容易将new[ ]与delete混用,例如:
typedef int Height[NUM];
int* pHeight = new Height;
//这种情况下用delete还是delete[ ]
delete pHeight; //错误
delete[] pHeight; //正确
建议不要对数组使用typedef, 可以使用STL中的vector代替数组。
3)构造函数中的new/new[ ]与析构函数中的delete/delete [ ]需一一对应
当类的成员中有指针变量时,在构造函数中用new申请空间,并且在析构函数中用delete释放空间是一种“标准的”、安全的做法。
3.6 智能指针
智能指针存储指向动态对象的指针,用于动态对象生存周期的控制,能够确保自动正确地销毁动态分配的对象,防止内存泄漏。
通俗来讲,智能指针就是模拟指针动作的类。所有的智能指针都会重载“->”和“*”操作符。智能指针的主要作用就是用栈智能指针离开作用域自动销毁时调用析构函数来释放资源。
智能指针的实现模板:
//智能指针的功能通常用类模板来实现
template <class T> class SmartPointer
{
private:
T *_ptr;
public:
SmartPointer(T *p):_ptr(p) //构造函数
{
}
T& operatror * () //重载*操作符
{
return *_ptr;
}
T* operator -> () //重载
{
return _ptr;
}
~SmartPointer() //析构函数
{
delete _ptr;
}
};
3.7 STL的四种智能指针
unique_ptr, auto_ptr, shared_ptr,和weak_ptr.
(1) unique_ptr
C++11引入,旨在替代不安全的auto_ptr,.
定义在<memory>中的智能指针,它对对象持有独有权----两个unique_ptr不能指向同一个对象,即其不共享其所管理的对象;
它无法复制到其他unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库(STL)算法,智能移动unique_ptr,
即对资源权限实现转移,这意味着内存资源所有权可以转移到另外一个unique_ptr中,并且原始的unique_ptr不再拥有此资源。
在实际应用中建议将对象限制为由一个所有者所有,因为多个所有权会使程序逻辑变得复杂。
因此当需要智能指针用于纯C++对象时,可使用unique_ptr;
unique_ptr指针与其所指对象的关系为:
在智能指针生命周期内可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、
通过移动语义转移所有权;
unique还可能没有对象,这种情况被称为empty。
//智能指针的创建
unique_ptr <int> u_i; //创建空智能指针
u_i.reset(new int(3)); //“绑定”动态对象
unique_ptr<int> u_i2(new int(4)); //创建时指定动态对象
(2) auto_ptr
(3) share_ptr
#include <iostream>
#include <memory>
using namespace std;
//基础对象类
class Point{
private:
int x;
int y;
public:
Point(int xVal = 0, int yVal = 0):x(xVal),y(yVal){}
int getX const()
{
return x;
}
int getY const()
{
return y;
}
void setX(int xVal)
{
x = xVal;
}
void setY(int yVal)
{
y = yVal;
}
};
//辅助类
class RefPtr{
private:
friend class SmartPtr;
RefPtr(Point *ptr):p(ptr),count(1){
}
~RefPtr(){delete p;}
int count;
Point *p;
};
//为基础对象类实现智能指针类
class SmartPtr{
public:
SmartPtr(Point *ptr):rp(new RefPtr(ptr)){}
SmartPtr(const SmartPtr &sp):rp(sp.rp){
++rp->count;
}
//重载赋值运算符
SmartPtr& operator=(const SmartPtr& rhs)
{
++rhs.rp->count;
if(--rp->count == 0)
delete rp;
rp = rhs.rp;
return *this;
}
//重载->操作符
Point * operator->()
{
return rp->p;
}
//重载*操作符
Point& operator*()
{
return *(rp->p);
}
~SmartPtr()
{
if(--rp->count == 0)
{
delete rp;
}
else
{
cout<<"还有"<<rp->count<<"个指针指向基础对象"<<endl;
}
}
private:
RefPtr *rp;
};
//智能指针类的使用与测试
int main()
{
//定义一个基础对象类指针
Point *pa = new Point(10,20);
//定义3个智能指针类对象,对象都指向基础类对象pa
//使用大括号控制3个智能指针的生命周期,观察计数的变化
{
SmartPtr sptr1(pa); //count=1
cout<<"sptr1:"<<sptr1->getX()<<","<<sptr1->getY()<<endl;
{
SmartPtr sptr2(sptr1); //调用拷贝构造函数,此时计数count=2
cout<<"sptr2:"<<sptr2->getX()<<","<<sptr2->getY()<<endl;
{
SmartPtr sptr3 = sptr1; //调用赋值操作符,此时计数count=3
cout<<"sptr3:"<<(*sptr3).getX()<<","<<(*sptr3).getY()<<endl;
}
//此时count = 2
}
//此时count = 1
}
//此时count = 0 ,pa对象被删除掉
cout<<pa->getX()<<endl;
return 0;
}
#include <iostream>
#include <memory>
using namespace std;
//模板类作为友元时要先声明
template <typename T> class SmartPtr;
//辅助类
template <typename T> class RefPtr{
private:
//该类成员权限为private,因为不想让用户直接使用该类
friend class SmartPtr<T>;
//构造函数的参数为基础对象的指针
RefPtr(T *ptr):p(ptr),count(1){
}
//析构函数
~RefPtr()
{
delete p;
}
//引用计数
int count;
//基础对象指针
T *p;
};
//智能指针类
template <typename T> class SmartPtr
{
public:
SmartPtr(T *ptr):rp(new RefPtr<T>(ptr)){} //构造函数
SmartPtr(const SmartPtr<T> &sp):rp(sp.rp){
++rp->count; //复制构造函数
}
SmartPtr& operator=(const SmartPtr<T>& rhs) //重载赋值构造函数
{
++rhs.rp->count; //首先将右操作数引用计数减1
if(--rp->count == 0) //然后将引用计数减1,可以应对自赋值
delete rp;
rp = rhs.rp;
return *this;
}
T& operator*()
{
return *(rp->p);
}
T* operator->()
{
return rp->p;
}
~SmartPtr()
{
if(--rp->count == 0) //当引用计数减为0时,删除辅助类对象指针,从而删除基础对象
{
delete rp;
}
else
{
cout<<"还有"<<rp->count<<"个指针指向基础对象"<<endl;
}
}
private:
RefPtr<T> *rp; //辅助类对象指针
};
int main()
{
//定义一个基础对象类指针
int *ia = new int(10);
{
SmartPtr <int> sptr1(ia);
cout<<"sptr1:"<<*sptr1<<endl;
{
SmartPtr <int> sptr2(sptr1);
cout<<"sptr2:"<<*sptr2<<endl;
*sptr2 = 5;
{
SmartPtr <int> sptr3 = sptr1;
cout<<"sptr3:"<<*sptr3<<endl;
}
//count=2
}
//count=1
}
//count = 0
cout<<*ia<<endl;
return 0;
}
(4) weak_ptr
weak_ptr被设计成与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造而来。
它的最大作用在于协助shared_ptr工作,可获得资源的观测权,像旁观者那样观测资源的使用情况。
观察者以为着weak_ptr只对share_ptr进行引用,而不改变其引用计数,当被观察的share_ptr失效后,
相应的weak_ptr也会失效。
weak_ptr <T> w; //创建空weak_ptr,可以指向类型为T的对象
weak_ptr <T> w(sp); //与shared_ptr指向相同的对象,shared_ptr引用计数不变,T必须能转换为sp指向的类型
w = p; //p可以是share_ptr或weak_ptr,赋值后w与p共享对象
w.reset(); //将w置空
w.use_count(); //返回与w共享对象的shared_ptr的数量
w.expired(); //若w.use_count()为0,则返回true,否则返回false
w.lock(); //如果expierd()为true,则返回一个空share_ptr,否则返回非空shared_ptr
3.7.5 如何选择智能指针
(1)如果程序要使用多个指向同一个对象的指针,那么应选择shared_ptr,。这样的情况包括:
A、 有一个指针数组,并使用一些辅助指针来标识特定的元素,如最大的元素和最小的元素
B、两个对象都包含指向第三个对象的指针
C、STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr
(编译器发出警告)和auto_ptr(行为不确定)。如果编译器没有提供share_ptr,那么可使用Boost库提供的share_ptr
(2)如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该
内存的指针,则将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接收返回值的unique_ptr,而该
智能指针将负责调用delete。可将unique_ptr存储当STL1容器中,只要不调用将一个unique_ptr复制或复制给另一个算法
(如sort())