C/C++进阶(一)--内存管理

news2024/11/28 19:37:18

 更多精彩内容.....

🎉❤️播主の主页✨😘

Stark、-CSDN博客

本文所在专栏:

学习专栏C语言_Stark、的博客-CSDN博客

其它专栏:

数据结构与算法_Stark、的博客-CSDN博客

​​​​​​项目实战C系列_Stark、的博客-CSDN博客

座右铭:梦想是一盏明灯,照亮我们前行的路,无论风雨多大,我们都要坚持不懈。


在前面,我们学习C语言的时候,我们就提到过内存的概念,介绍了内存的分区以及各个区域负责的事情,并且给出了一些内存函数使用。

C语言的内存知识_c语言内存-CSDN博客

本节我们主要是学习内存的管理。


一、内存分区模型

一般我们将内存划为四个或五个区,而真实的内存远远不止这四五个区。我们先来回顾一下内存分区的知识:

Ⅰ.栈区(stack):由编译器自动分配释放。存放函数的参数值,局部变量的值等。

Ⅱ.堆区(heap):一般由程序员分配释放(动态内存申请和释放)。若程序员不释放,程序结束时可能由操作系统回收。

Ⅲ.全局区(global)/静态区(static):全局变量和静态变量是放在一块的,初始化的全局变量和静态变量在一块儿区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块儿区域,该区域在程序结束后由操作系统释放。

Ⅳ.常量区(const):字符串常量和其它常量的存储位置,程序结束后操作系统回收

Ⅴ.程序代码区(code):存放函数体的二进制代码

下面我们给出一段代码进行一下简单的验证:

int g_a;//全局变量
int g_b;
static int s_g_a;//静态全局变量
static int s_g_b;
const int c_g_a=1;//全局常量
const int c_g_b=1;
const static int c_s_g_a;//静态全局常量
const static int c_s_g_b;
int main() {
	cout << "全局变量" << endl;
	cout << &g_a << endl;
	cout << &g_b << endl;
	cout << "静态全局变量" << endl;
	cout << &s_g_a << endl;
	cout << &s_g_b << endl;
	cout << "全局常量" << endl;
	cout << &c_g_a << endl;
	cout << &c_g_b << endl;
	cout << "静态全局常量" << endl;
	cout << &c_s_g_a << endl;
	cout << &c_s_g_b << endl;

	int l_a;//局部变量
	int l_b;
	cout << "局部变量" << endl;
	cout << &l_a << endl;
	cout << &l_b << endl;
	const int c_l_a=1;//局部常量
	const int c_l_b=1;
	cout << "局部常量" << endl;
	cout << &c_l_a << endl;
	cout << &c_l_b << endl;
	static int s_l_a;//静态局部变量
	static int s_l_b;
	cout << "静态局部变量" << endl;
	cout << &s_l_a << endl;
	cout << &s_l_b << endl;
	const static int c_s_l_a;//静态局部常量
	const static int c_s_l_b;
	cout << "静态局部常量" << endl;
	cout << &c_s_l_a << endl;
	cout << &c_s_l_b << endl;
	//字面量(字符串常量)
	cout << "字面量地址:" << &("123") << endl;
	return 0;
}

从中,我们不难发现,本次运行时,全局区的数据全部在00007FF69DC5开头的内存地址处,并且静态区的也是放在了这段内存中,而且:变量在+F1+段,常量在+BB+段。另外不被const修饰的普通常量(字符串字面量)的地址也存在了00007FF69DC5这段内存中,且在+C010处。综上分析,我们可以知道: 大致就是这么一个分布。


 

Linux系统由操作系统预留空间,一般默认8MWindows系统由编译器预留空间,其中VC++一般默认1M。当我们使用栈区空间时,都是比较节俭的,所以我们程序员一般会主动向堆区申请空间,以免栈溢出

如果把内存分为四个区的话,一般不包括常量区。而其它四区的建立流程是:

 流程说明:操作系统把物理硬盘代码load到内存=>操作系统把c代码分成四个区=>操作系统找到main函数入口执行。


二、函数调用模型

C语言的函数-CSDN博客

在上面将函数的时候我们提过函数栈帧的概念。那么这就是函数调用的模型。学好之后对函数的递归学习有很大帮助哦。

1、 一个主程序有n个函数组成,C++编译器会建立有几个堆区?有几个堆区?

2、函数嵌套调用时,实参地址传给形参后,C++编译器如何管理变量的生命周期?

分析:通过函数fa(),调用函数fb(),通过参数传递的变量,内存空间能用嘛?


三、内存管理

在前文的内存知识章节我们只说了内存函数,没有说内存管理函数。对于C语言,内存管理函数包括:malloc,realloc,calloc,以及free四个。包含在头文件<stdlib.h>中。

1.malloc()函数

用处:申请堆区空间

函数原型:void* malloc(size_t _Size)

函数解析:参数是需要申请的空间字节大小,返回值是void*类型的指针,方便类型转换为其他类型的指针。

使用例子:

int *p=(int*)malloc(sizeof(int)*5);//申请能存放五个int类型数据的空间
//相当于一个存在于堆区的数组int [5],但不是。

2.realloc()函数

用处:申请的空间不足,需要扩容

函数原型:void* realloc(void *_Block,size_t _Size)

函数解析:void*的用处全都是方便类型转换,_Size是字节大小。_Block是需要扩容的指针

使用例子:

int * p=(int*)malloc(sizeof(int)*4);
for(int i=0;i<4;i++){
    p[i]=i;
}
//此时我想对p[4]进行赋值,明显已经越界了,那我就扩容
p = (int*)realloc(p,sizeof(int)*8);
p[4]=15;

3.calloc()函数

用处:开辟空间的同时,赋予一定数量的内存一个初始默认值

函数原型:void* calloc(size_t _Count,size_t _Size)

函数解析:类似于malloc,不过参数多了一个_Count,这个参数的意思是,在申请空间的同时,为前Count块内存赋予初始默认值。

使用例子:

int n;cin>>n;//准备开辟多少 个 空间。
int *p=(int*)calloc(n,sizeof(int)*n);//个数*每个所占字节=总字节
for(int i=0;i<n;i++){
    cout<<p[i]<<" ";
}

4.free()函数

用处:释放申请的堆区空间

函数原型:void free(void* _Block)

函数解释:将_Block指向的堆区空间进行释放。

使用例子:

free(p);//p为上面申请的指针的任意一个

初识new与delete

C++中的new运算符和delete运算符用于在堆上动态分配和释放内存。堆上的内存由程序员手动管理,可以在程序的任意位置进行分配和释放。

new运算符的语法如下:

new type;
new type[size];

其中,type可以是任意的数据类型,size是一个整数,表示要分配的数组大小。

new运算符会在堆上分配一块内存,并返回一个指向该内存的指针。如果分配成功,new运算符将返回一个指向type的指针;如果分配失败,new运算符将抛出std::bad_alloc异常。

例如,下面的代码分配了一个int数组:

int* arr = new int[10];

delete运算符的语法如下:

delete ptr;
delete[] ptr;

其中,ptr是由new运算符返回的指针。

delete运算符会释放ptr指向的内存,并调用析构函数来销毁对象(如果有的话)。如果ptr为nullptr,则delete运算符不会进行任何操作。

如果ptr是由new[]运算符返回的指针,则必须使用delete[]运算符来释放内存。如果使用delete运算符来释放由new[]分配的内存,行为是未定义的。

例如,下面的代码释放了前面分配的int数组:

delete[] arr;

需要注意的是,使用new运算符分配的内存必须使用delete运算符来释放,否则就会出现内存泄漏。同样地,使用new[]运算符分配的数组必须使用delete[]运算符来释放。

另外,还可以使用placement new运算符在已经分配的内存上构造对象,以及使用delete运算符销毁对象并释放内存。但这种用法相对较少见,一般只在特定场景下使用。

下面是一个完整的示例,展示了如何使用new与delete运算符:

#include <iostream> 
class MyClass { 
public: 
    MyClass(int val) : value(val) { 
        std::cout << "Constructor called: " << value << std::endl; 
    }
    ~MyClass() {
        std::cout << "Destructor called: " << value << std::endl; 
    } 
private: 
    int value; 
}; 
int main() { 
    // 动态分配单个对象 
    MyClass* obj = new MyClass(10); 
    // 动态分配数组 
    MyClass* arr = new MyClass[3] { MyClass(1), MyClass(2), MyClass(3) }; 
    // 释放单个对象 
    delete obj; 
    // 释放数组 
    delete[] arr; 
    return 0; 
}

在这个示例中,我们定义了一个简单的类MyClass,并展示了如何使用new运算符分配内存和delete运算符释放内存。

剖析new与delete

(本段内容为转载:深入理解C++ new/delete, new []/delete[]动态内存管理 - tp_16b - 博客园)

在C语言中,我们写程序时,总是会有动态开辟内存的需求,每到这个时候我们就会想到用malloc/free 去从堆里面动态申请出来一段内存给我们用。但对这一块申请出来的内存,往往还需要我们对它进行稍许的“加工”后即初始化 才能为我们所用,虽然C语言为我们提供了calloc来开辟一段初始化好(0)的一段内存,但面对象中各是各样的数据成员初始化,它同样束手无策。同时,为了保持良好的编程习惯,我们也都应该对申请出来的内存作手动进行初始化。

对此,这常常让我们感到一丝繁琐,于是到了C++中就有了new/delete, new []/delete[] 。用它们便可实现动态的内存管理。

 在C++中,把int 、char..等内置类型的变量也看作对象,它们也是存在构造函数和析构函数的,只是通常对它们,系统调用了默认的构造函数来初始化以及默认的析构(编译器优化)。所以new int、new int(3)看起来和普通的定义好像没什么区别。 但对于自定义类型的对象,此种方式在创建对象的同时,还会将对象初始化好;于是new/delete、new []/delete []方式管理内存相对于malloc/free的方式管理的优势就体现出来了,因为它们能保证对象一被创建出来便被初始化,出了作用域便被自动清理。

  *  malloc/free只是动态分配内存空间/释放空间。而new/delete除了分配空间还会调用构造函数和析构函数进行初始化与清理(清理成员)。

  *  它们都是动态管理内存的入口。
  *  malloc/free是C/C++标准库的函数,new/delete是C++操作符
  *  malloc/free需要手动计算类型大小且返回值w为void*,new/delete可自动计算类型的大小,返回对应类型的指针。

  *  malloc/free管理内存失败会返回0,new/delete等的方式管理内存失败会抛出异常。


四、常见内存错误

1.内存泄露

概念:内存泄漏(Memory Leak)是指程序在运行过程中,未能释放已经分配的内存空间,从而导致可用内存减少的现象。随着时间的推移,内存泄漏会导致系统性能下降,甚至可能导致程序崩溃。

内存泄漏一般发生在以下情况下:

  1. 动态分配内存但未释放:使用如malloc/new(在C/C++中)申请内存后,如果没有相应的free/delete 调用便不会释放这块内存。

  2. 引用计数错误:在使用引用计数管理内存的情况下,如果对象之间形成循环引用,可能导致对象的内存无法释放。

  3. 不适当地存储指针:程序在某个数据结构中存储了对动态分配内存的指针,但在不再需要该数据结构时,没有释放内存。

为防止内存泄漏,开发者可以采取以下措施:

  • 使用智能指针(在C++中,可自动管理内存)。
  • 定期检查和分析代码,寻找潜在的内存泄漏。
  • 使用内存检测工具(如Valgrind等),识别和报告内存泄漏问题。
  • 确保在不再需要对象时适时释放内存

总结:申请的内存空间没有被释放掉。及时发现和修复内存泄漏问题,可以提高程序的稳定性和性能。

2.栈溢出

概念:栈溢出(Stack Overflow)是一种运行时错误,发生在程序使用的栈空间超过了栈所分配的大小时。栈是用于存储局部变量和函数调用信息的一块内存区域,每当一个函数被调用时,相关的局部变量和返回地址等信息会被压入栈中;当函数执行完毕时,这些信息会被弹出栈外。

栈溢出的常见原因包括:

  1. 深度递归:当一个函数递归调用自身且没有适当的终止条件时,可能导致栈帧不断增加,最终消耗完分配给栈的内存。

  2. 过大的局部变量:在函数中声明过大的数组或数据结构,也可能导致栈空间不够。

  3. 无限循环或错误的循环逻辑:在某些情况下,如果循环体中的函数调用未能适当退出,也可能引发栈溢出。

栈溢出会导致程序崩溃,通常会触发操作系统的保护机制,从而抛出异常或错误信息。为防止栈溢出,可以采取以下措施:

  • 避免不必要的深度递归:使用迭代法替代递归,或者优化递归算法以减少栈深度。

  • 合理使用局部变量:尽量避免在栈上使用过大的数据结构,考虑使用堆分配(如动态分配内存)。

  • 监测和调试用:使用调试工具跟踪栈的使用情况,识别潜在的栈溢出风险。

  • 控制递归深度:合理设计递归的深度和适当的退出条件。

总结:存在栈区的数组开辟太大,递归层次太深。有效减少栈溢出的发生,确保程序的稳定性和可靠性。

3.越界访问与野指针

越界访问:在数组或存储块的边界之外进行读或写操作。可能导致数据损坏或安全漏洞。

野指针:访问未初始化的指针会导致不可预测的行为,因为它指向未知的内存区域。

4.双重释放与悬挂指针

悬挂指针:指向已经释放的内存地址的指针。使用这种指针进行访问会导致未定义行为。悬挂指针通常是在调用deletefree后未将指针置为nullptr导致的。

双重释放:指同一块内存被deletefree多次释放,这会导致未定义行为,甚至程序崩溃。

5.其他情况

其它情况还包括:使用new而不配对delete,使用malloc而不配对free,或者使用new开辟多块空间没有使用delete[]。

不当使用智能指针:虽然智能指针(如std::unique_ptrstd::shared_ptr)可以帮助管理内存,但不正确的使用仍然可能导致悬挂指针、循环引用(在std::shared_ptr中)等问题。


感谢大家。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2186633.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

免费录屏软件工具:助力高效屏幕录制

录屏已经成为了一项非常实用且广泛应用的技术。无论是制作教学视频、记录游戏精彩瞬间&#xff0c;还是进行软件操作演示等&#xff0c;我们都常常需要一款可靠的录屏软件。今天&#xff0c;就让我们一起来探索那些功能强大录屏软件免费版&#xff0c;看看它们是如何满足我们多…

ARTS Week 42

Algorithm 本周的算法题为 2283. 判断一个数的数字计数是否等于数位的值 给你一个下标从 0 开始长度为 n 的字符串 num &#xff0c;它只包含数字。 如果对于 每个 0 < i < n 的下标 i &#xff0c;都满足数位 i 在 num 中出现了 num[i]次&#xff0c;那么请你返回 true …

【数据结构强化】应用题打卡

应用题打卡 数组的应用 对称矩阵的压缩存储 注意&#xff1a; 1. 2.上三角的行优先存储及下三角的列优先存储与数组的下表对应 上/下三角矩阵的压缩存储 注意&#xff1a; 上/下三角压缩存储是将0元素统一压缩存储&#xff0c;而不是将对角线元素统一压缩存储 三对角矩阵的…

接口隔离原则在前端的应用

什么是接口隔离 接口隔离原则&#xff08;ISP&#xff09;是面向对象编程中的SOLID原则之一&#xff0c;它专注于设计接口。强调在设计接口时&#xff0c;应该确保一个类不必实现它不需要的方法。换句话说&#xff0c;接口应该尽可能地小&#xff0c;只包含一个类需要的方法&am…

SKD4(note上)

微软提供了图形的界面API&#xff0c;叫GDI 如果你想画某个窗口&#xff0c;你必须拿到此窗口的HDC #include <windows.h> #include<tchar.h> #include <stdio.h> #include <strsafe.h> #include <string>/*鼠标消息 * 键盘消息 * Onkeydown * …

实验 3 存储器实验

实验 3 存储器实验 1、实验目的 掌握静态随机存储器 RAM 的工作特性。掌握静态随机存储器 RAM 的读写方法。 2、实验要求 (1)做好实验预习&#xff0c;熟悉MEMORY6116 芯片各引脚的功能和连接方式&#xff0c;熟悉其他实验元器件的功能特性和使用方法&#xff0c;看懂电路图…

CSS | 响应式布局之媒体查询(media-query)详解

media type(媒体类型)是CSS 2中的一个非常有用的属性&#xff0c;通过media type我们可以对不同的设备指定特定的样式&#xff0c;从而实现更丰富的界面。media query(媒体查询)是对media type的一种增强&#xff0c;是CSS 3的重要内容之一。随着移动互联网的发展&#xff0c;m…

中国靠谱热门交友软件app排行榜前十名测评

在信息飞速发展的时代&#xff0c;交友软件层出不穷。究竟哪些才是靠谱又热门的呢&#xff1f;这份交友软件 app 排行榜将为你揭晓&#xff0c;带你走进不同的社交天地&#xff0c;开启精彩交友之旅。 咕哇找搭子小程序&#xff1a;这是一个实名制的找搭子交友平台&#xff0c;…

基于ssm的学生社团管理系统 社团分配系统 社团活动调度平台 学生社团管理 信息化社团管理开发项目 社团活动管理 社团预约系统(源码+文档+定制)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

时尚科技融合:Spring Boot下的“衣依”服装销售平台

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常适…

虚拟机三种网络模式详解

在电脑里开一台虚拟机&#xff0c;是再常见不过的操作了。无论是用虚拟机玩只有旧版本系统能运行的游戏&#xff0c;还是用来学习Linux、跑跑应用程序都是很好的。而这其中&#xff0c;虚拟机网络是绝对绕不过去的。本篇文章通俗易懂的介绍了常见的虚拟网络提供的三种网络链接模…

小红书AI配音神器:3秒变声百种风格

小红书AI配音神器&#xff1a;3秒变声百种风格 小红书推出黑科技FireRedTTS&#x1f3a4;&#xff0c;3秒克隆你的声音✨&#xff0c;支持搞怪、温柔等多种风格&#x1f389;&#xff01;只需几秒参考音频&#xff0c;轻松生成个性化语音&#xff0c;短视频配音神器&#x1f3…

把白底照片变蓝色用什么软件免费 批量更换证件照底色怎么弄

作为专业的修图师&#xff0c;有时候也会接手证件照修图和换底色工作&#xff0c;这种情况下&#xff0c;需要换底色的照片也许达到上百张。为了提高工作效率&#xff0c;一般需要批量快速修图&#xff0c;那么使用什么软件工具能够给各式不同的照片批量更换背景色呢&#xff1…

Python并发编程(1)——Python并发编程的几种实现方式

更多精彩内容&#xff0c;请关注同名公众&#xff1a;一点sir&#xff08;alittle-sir&#xff09; Python 并发编程是指在 Python 中编写能够同时执行多个任务的程序。并发编程在任何一门语言当中都是比较难的&#xff0c;因为会涉及各种各样的问题&#xff0c;在Python当中也…

【Unity AI】基于 WebSocket 和 讯飞星火大模型

文章目录 整体AIManagerDialogueManagerUIManagerModelManagerAudioManagerSaveManager 详细部分AIUI动画音频 整体 AIManager 负责配置讯飞的appId&#xff0c;生成鉴权URL&#xff0c;通过WebSocket向服务器请求并返回数据&#xff08;分为最终返回和流式返回&#xff09; …

C++基础(6)——模板初阶

目录 1.泛型编程 2.函数模板 2.1函数模板的概念 2.2 函数模板格式 2.3 函数模板的原理 2.4 函数模板的实例化 2.4.1隐式实例化&#xff1a;让编译器根据实参推演模板参数的实际类型 2.4.2显式实例化&#xff1a;在函数名后的<>中指定模板参数的实际类型 2.5 模板…

C++ WebDriver扩展

概述 WebDriver协议基于HTTP&#xff0c;使用JSON进行数据传输&#xff0c;定义了client与driver之间的通信标准。无论client的实现语言&#xff08;如Java或C#&#xff09;&#xff0c;都能通过协议中的endpoints准确指示driver执行各种操作&#xff0c;覆盖了Selenium的所有功…

Redis入门第五步:Redis持久化

欢迎继续跟随《Redis新手指南&#xff1a;从入门到精通》专栏的步伐&#xff01;在本文中&#xff0c;我们将深入探讨Redis的持久化机制&#xff0c;这是确保数据在服务器重启后不会丢失的关键功能。了解如何配置和使用不同的持久化方法&#xff0c;对于构建可靠的应用程序至关…

数据库(MySQL):使用命令从零开始在Navicat创建一个数据库及其数据表(二)

前言 在上一节中&#xff0c;主要介绍了 Navicat Premium 17 的使用以及创建一个基础的表格。当时只设置了给数据表补充字段&#xff0c;没有设置给数据表删除字段。现在补充一下。 ALTER TABLE student ADD test int(4); 给名为 student 的数据表添加 test 列&#xf…

CI/CD中的自动化测试:在持续集成/持续部署流程中引入网页自动化测试

目录 引言 一、CI/CD流程概述 1.1 什么是CI/CD 1.2 CI/CD流程的主要阶段 1.3 CI/CD的优点 二、自动化测试基础 2.1 自动化测试概述 2.2 自动化测试的作用 2.3 自动化测试的主要类型 三、Web自动化测试工具 3.1 Selenium 3.1.1 Selenium WebDriver常用API 3.1.2 示例…