C++中的栈和堆

2023-10-31

由C/C++编译的程序占用的内存分为以下几个部分:

 1、栈区(stack):又编译器自动分配释放,存放函数的参数值,局部变量的值等,其操作方式类似于数据结构的栈。
 2、堆区(heap):一般是由程序员分配释放,若程序员不释放的话,程序结束时可能由OS回收,值得注意的是他与数据结构的堆是两回事,分配方式倒是类似于数据结构的链表。
 3、全局区(static):也叫静态数据内存空间,存储全局变量和静态变量,全局变量和静态变量的存储是放一块的,初始化的全局变量和静态变量放一块区域,没有初始化的在相邻的另一块区域,程序结束后由系统释放。
 4、文字常量区:常量字符串就是放在这里,程序结束后由系统释放。
 5、程序代码区:存放函数体的二进制代码。

一、栈空间

1.1 自动释放内存无需码农操作

  在程序中定义的局部变量(包括了指针类型变量)、局部数组等都是存储在栈空间中。栈空间具有一个鲜明的特点:函数内定义的变量出了函数范围,其所占用的内存空间自动释放。但是,栈空间的尺寸有最大限制,不适合分配大空间使用

  所以,因为栈空间出了函数范围就释放,所以不适合要给其他地方使用的内存需求。其最大的好处就在于:不用程序员手动释放内存。

参考我们上一篇文章给出的例子:

CTest* fun(CBest* pBest)
{
   CTest* pTest = new CTest();//局部变量,虽然使用了new,但是我们不用手动释放(出了这个函数也没地方释放啊)
   pTest->a = pBest->b;
   return pTest;
}

1.2 不要把局部变量的指针做为返回值返回

  首先,我们来看看下面一段代码,其中getData函数返回了一个int数组类型的指针,而getData2函数返回了另一个int数组类型的指针:

int *getData()
{
    int nums[10]={1,2,3,4,5,6,7,8};//在栈中
    return nums;
}
 
int *getData2()
{
    int aaa[10]={8,7,6,5,4,3,2,1};//在栈中
    return aaa;
}

int main(int argc, char *argv[])
{
    int *nums = getData();
    printf("%d,%d,%d\n",nums[0],nums[1],nums[2]);//可以正常返回
    return 0;
}

我们修改一下main函数:

int main(int argc, char *argv[])
{
    int *nums = getData();
    getData2();
    printf("%d,%d,%d\n",nums[0],nums[1],nums[2]);

    return 0;
}

上述测试在某些系统是不能正常执行的(例如在win7+codeblocks下,可以编译通过(编译warning getData()返回局部变量:address of local variable 'aaa' returned ),但执行报错)

这显然是不正常的!为什么呢?

这是因为栈是由系统自动分配和释放(局部数组是分配在栈中的,出了函数,局部数组就会被释放掉),函数内部的局部变量的生命周期就只是在函数周期内,只要函数执行完毕,那么其内部的局部变量的生命周期也就结束了。于是,当我们执行完第一句代码后,nums指针所指向的数组的那一块内存区域(getData()中的指针变量nums指向的长度为10的区域)可能就已经被释放了,但是数据还未清理也就是还留在那儿。但是,当我们执行完第二句代码后,在getData2函数中又定义了一个数组aaa,它又将刚刚释放的栈空间内存占用了,于是main中的nums所指向的这块区域(main中的nums指针是正常的,它还是有值的,并且值没有发生变化,还是指向某个位置,但是其指向的位置的内容可能已经变了,原来指向getData()中的nums数组所在的位置,由于getData()执行完后,nums数组释放。后面调用getData2(),里面申请了aaa数组,这个aaa就可能占用了原来getData()中的nums数组对应的区域,然后就悲剧了)就是aaa了。当执行完第二句代码,aaa又被释放了,但是其数据还在那里并未清除,也就是我们前面几篇提到的脏内存区域。所以,最后显示的就是8,7,6而不是1,2,3了。 

二、堆空间

2.1 用户掌握堆的分配

栈空间最大的优点就是栈空间出了函数范围就释放,不需要程序员手动释放。但是,如果我们想自己控制内存的分配呢?这时候,就可以使用堆空间来存储,堆空间可以存储栈空间无法存储的大内存。这里,我们可以借助malloc函数在堆空间中分配一块指定大小的内存,用完之后,调用free函数及时释放内存。

  // malloc(要分配的字节数)
    int *nums = (int*)malloc(sizeof(int)*10);
    nums[0]=1;
    nums[1]=8;
    free(nums);

  需要注意的是:在malloc函数中需要指定要分配的内存所占用的字节大小。当然也可以用new和delete来实现内存的申请和释放。

2.2 函数返回指针的几种解决办法

  (1)在方法内malloc,用完了由调用者free

  这里我们可以结合malloc和free来解决我们在栈空间中所遇到的问题,重写上面的代码如下:

int *getData()
{
    int *nums = (int*)malloc(sizeof(int)*10);//在堆中申请10个int长度的空间
    nums[0]=1;
    nums[1]=8;
    nums[2]=3;

    return nums;
}

int *getData2()
{
    int *nums = (int*)malloc(sizeof(int)*10);//在堆中申请10个int长度的空间
    nums[0]=2;
    nums[1]=7;
    nums[2]=5;

    return nums;
}

int main(int argc, char *argv[])
{
    int *numsptr = getData();
    int *numsptr2 = getData2();或者getData2();
    // numptr[1]等价于*(numptr+1)
    printf("%d,%d,%d\n",numsptr[0],numsptr[1],numsptr[2]);
    // 不要忘记释放内存
    free(numsptr);
    free(numsptr2);

    return 0;
}

这次可以正常执行,返回的都是1,2,3.输出的还是getData函数返回的指针所指向的内存区域的数据,没有出现交叉影响,完美!

(2)把局部变量定义为static

char *getStr()
{
    static char strs[]="afsafdasfdsdfsaddafafafasdfadfs";
    return strs;
}

int main(int argc, char *argv[])
{
    char* strsptr = getStr();

    return 0;
}

strs存放在全局区,它直到程序结束后才会释放。But,需要注意的是:不适合于多线程调用,如果想保存返回内容,你需要调用者尽快复制一份。

(3)(推荐)由调用者分配内存空间,只是把指针发给函数,函数内部把数据拷贝到内存中

  这里怎么来理解呢,也就是三个步骤,第一步:由调用者分配内存空间;第二步:把指针传递给函数;第三步:函数内部把数据拷贝到内存中。下面我们通过一个小案例:从文件名分析文件名和扩展名,来看看这三个步骤怎么来实现。

// Step3:函数内部把数据拷贝到内存中
void parseFileName(char* filename,char* name,char* ext)
{
    char *ptr = filename;
    while(*ptr != '\0')
    {
        ptr++;
    }
    // 记录结尾的指针
    char *endPtr = ptr;

    //ptr移动到了字符串的结尾,再把ptr移动到"."的位置
    while(*ptr != '.')
    {
        ptr--;
    }

    // 两个指针相减表示两个指针相隔的元素的个数
    memcpy(name,filename,(ptr-filename)*sizeof(char));
    memcpy(ext,ptr+1,(endPtr-ptr)*sizeof(char));
}

int main(int argc, char *argv[])
{
    // Step1:由调用者分配内存空间
    char str[] = "[TK-300]美.女.avi"; 
    char name[20] = {0};
    char ext[20] = {0};
    // Step2:只是把指针传递给函数
    parseFileName(str,name,ext);
    printf("解析完成:\n");
    printf("文件名:%s,后缀:%s\n",name,ext);
    
    return 0;
}

这种方法避免了函数返回指针,程序员手动分配的内存都是在栈空间中,然后函数内部处理后再将经过逻辑处理后的数据存储到栈空间中的指定区域内,最后main函数中再访问修改后的内存区域。这里的运行结果如下图所示:

最后注意:

1. new出来的空间,要用delete释放掉;

2. 释放的是申请到的内存区域 ,而不是指针;

3.释放掉的是指针指向的那个区域的内容,而不是释放掉指针本身;

4.如果不做指针置为NULL的操作,则指针还指向原来的内存位置,如果该位置的内容发生变化,则再用指针,可能会出错。

参考:

http://www.cnblogs.com/edisonchou/p/4669098.html 

如鹏网,《C语言也能干大事(第三版)》

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++中的栈和堆 的相关文章

随机推荐

  • cocos2d-x系列教程

    1 官网 http www cocos com 2 论坛 http www cocoachina com bbs thread php fid 48 3 官网下载 http www cocos2d x org download versio
  • 【如何注销华为云账号(解绑合作伙伴)】

    如何注销华为云账号 解绑合作伙伴 一 解绑合作伙伴 1 1 注意事项 1 2 创建工单 二 账号关闭 三 账号注销 一 解绑合作伙伴 首先进入华为云官网 https www huaweicloud com 登录账号 将鼠标拖至页面右上角 选
  • MySQL 事务

    事务特性 ACID 事务隔离中的脏读 幻读问题 隔离级别 一 事务特性 ACID 原子性 Atomicity 一个事务内多个操作语句 不可被分割 一个事务的执行结果 要么全部成功 要么全部失败 一致性 Consistency 一个事务操作之
  • CURL 宏定义列表

    列表CURL库一共有17个函数 curl close 关闭CURL会话 curl copy handle 复制一个CURL会话句柄 同时3复制其所有参数 curl errno 返回最后一个错误码 curl error 返回一个字符串 用以描
  • uboot启动之BL1阶段的分析1

    对uboot启动的BL1阶段的主体代码分析1 BL1阶段代码的分析以start s文件作为主要的目标 此篇博文主要对整个个流程进行分析 总体分析 BL1阶段的代码固化在IROM中的BL0调用执行 在上电之后会会执行 他的主要主要工作就是初始
  • 浮动元素与定位元素的关系

    div class a div div class b div 两种情况 a不浮动 在元素a不浮动的情况下 元素a并没有脱离文档流
  • VS2019-C++创建和调用DLL动态链接库(傻瓜式教程)

    转载地址 前言 查了好久好久网上的资料C 调用动态链接库 试了好多方法 直接创建DLL 空项目创建的 都多多少少有些问题 最后自己不断摸索着成功了 还是很开心的 接下来把最清晰的步骤分享给大家 C 新人 有什么说的不清楚的还请大佬们见谅 第
  • oracle 删除所有外键

    1 执行语句 select alter table table name drop constraint constraint name from user constraints where constraint type R 2 执行第
  • 跨品种套利 - 期货

    1 原理 什么是套利 套利是指在买入或卖出一种金融资产的同时卖出或买入另一种相关的金融资产从中利用价差获得套利的过程 什么是跨品种套利 当两个合约有很强的相关性时 可能存在相似的变动关系 两种合约之间的价差会维持在一定的水平上 当市场出现变
  • 【burpsuite安全练兵场-客户端14】点击劫持-5个实验(全)

    前言 介绍 博主 网络安全领域狂热爱好者 承诺在CSDN永久无偿分享文章 殊荣 CSDN网络安全领域优质创作者 2022年双十一业务安全保卫战 某厂第一名 某厂特邀数字业务安全研究员 edusrc高白帽 vulfocus 攻防世界等平台排名
  • IDEA test程序无法输入 This view is read-only 解决办法

    问题 test程序无法输入 按回车键显示如下 解决办法 在IDEA Help gt Edit Custom VM Options 中添加如下代码 然后重启IDEA即可 Deditable java test console true
  • 5-7 12-24小时制

    编写一个程序 要求用户输入24小时制的时间 然后显示12小时制的时间 输入格式 输入在一行中给出带有中间的 符号 半角的冒号 的24小时制的时间 如12 34表示12点34分 当小时或分钟数小于10时 均没有前导的零 如5 6表示5点零6分
  • python输入10个数求偶数奇数之和_从键盘上输入十个整数,并求出这十个数所有奇数之和及偶数之和。C++编程,要求有do while,w...

    展开全部 include int main int i int num int jishu oushu do while i 0 jishu oushu 0 do scanf d num if num 2 0 oushu num else
  • mysql grouping sets_mysql – PostgreSQL:如何使用GROUPING SETS,CUBE...

    我有以下在MySQL 5 6中编写的代码块 INSERT INTO Totals SELECT Zone State COUNT Sponsored COUNT Enrolled COUNT PickedUp FROM MasterData
  • HTTP头的Expires与Cache-control

    HTTP头的Expires与Cache control HTTP头的Expires与Cache control 1 概念 Cache control用于控制HTTP缓存 在HTTP 1 0中可能部分没实现 仅仅实现了Pragma no ca
  • 零基础学SQL(1):初识数据库与SQL

    零基础学SQL 1 初识数据库与SQL 一 初识数据库 数据库是将大量数据保存起来 通过计算机加工而成的可以 进行高效访问的数据集合 该数据集合称为数据库 Database DB 用来管理数据库的计算机系统称为数据库管理系统 Databas
  • ionic3+angular4 HttpClient封装优化

    背景 众所周知 angular4以后引入的HTTPClient为前端http请求带来了非常大的改进 它支持了与java类似的拦截器机制 通过拦截器 可以方便的对请求进行前置 后置及异常处理 但如果我们需要在不同的项目中使用这个拦截器 且有不
  • pt-online-schema-change添加索引没有反应

    之前使用 pt online schema change添加索引没有问题 如下语句 root ixxxxxx pt online schema change no version check execute alter foreign ke
  • WSL使用技巧 / 虚拟机对比

    WSL使用技巧 虚拟机对比 前言 虚拟机比较 VMware使用技巧 WSL使用技巧 官方文档 工具 安装WSL 基本命令 运行命令 关闭卸载 磁盘管理 导入导出 指定安装路径 前言 本文介绍了VMware和WSL的区别 并详细介绍了WSL的
  • C++中的栈和堆

    由C C 编译的程序占用的内存分为以下几个部分 1 栈区 stack 又编译器自动分配释放 存放函数的参数值 局部变量的值等 其操作方式类似于数据结构的栈 2 堆区 heap 一般是由程序员分配释放 若程序员不释放的话 程序结束时可能由OS