[C++核心编程-01]----C++内存四区详细解析

前言        

        当程序运行时,操作系统会为程序分配一块内存空间,这块内存空间被划分为不同的区域,每个区域有其独特的作用和管理方式。四个区域分别为:堆、栈、全局/静态存储区和常量存储区。每个区域都有不同的作用和特点,下面分别进行详细介绍。

正文

01-内存区域简介    

        当程序运行时,操作系统会为程序分配一块内存空间,这块内存空间被划分为不同的区域,每个区域有其独特的作用和管理方式。下面详细说明一下四个内存区域:

        堆(Heap)

        a、堆是由程序员进行手动管理的内存区域,可以动态分配和释放内存。

        b、在堆上分配内存使用 new 关键字,释放内存使用 delete 关键字。

        c、堆上的内存分配由程序员控制,可以动态调整大小,但需要确保正确释放分配的内存,否则会出现内存泄漏问题。堆上的内存分配速度较慢,不适合频繁分配小块内存。

        栈(Stack)

        a、栈是由系统自动管理的内存区域,用于存储函数调用信息和局部变量。

        b、每次函数调用时,系统会为函数分配一块栈帧,用于存储该函数的局部变量和返回地址等信息。

        c、栈上的内存是有限的,栈的大小在程序启动时就已经确定,递归调用过多或者分配过大的局部变量可能会导致栈溢出。

        全局/静态存储区(Global/Static Storage Area)

        a、全局变量和静态变量存储在这个区域中,其生命周期从程序开始到程序结束。

        b、全局变量存储在全局初始化数据段中,静态变量存储在全局未初始化数据段中。

        c、全局变量可以在任何地方访问,但应该避免过度使用全局变量,因为全局变量容易导致程序的耦合度增加。

        常量存储区(Constant Storage Area)

        a、常量数据(如字符串常量)存储在常量存储区,是只读的,程序不能修改这些数据。

        b、常量存储区通常位于全局/静态存储区的静态内存中,也可能位于代码段中,取决于编译器和操作系统的实现。

02-全局区    

        全局区(Global/Static Storage Area)是C++程序中存储全局变量和静态变量的内存区域。在程序运行期间,全局区中的变量始终存在,其生命周期从程序开始到程序结束。全局区分为两部分:全局初始化数据段和全局未初始化数据段。

        全局初始化数据段:存储已经初始化的全局变量和静态变量的数值,这些变量在编译时就被定义并初始化了。

        全局未初始化数据段:存储未初始化的全局变量和静态变量,这些变量会在程序运行时被初始化为0或者空值。

        下面给出一部分代码展示全局变量在全局区中的使用:在这个示例中,global_init_varstatic_global_init_var是全局初始化变量,它们的值在编译时已经确定;global_uninit_var是全局未初始化变量,它会在程序运行时被自动初始化为0。在main函数中,我们可以随时访问和修改这些全局变量的值。

#include <iostream>
using namespace std;

// 全局变量,在全局初始化数据段
int global_init_var = 10;

// 静态全局变量,在全局初始化数据段
static int static_global_init_var = 20;

// 全局未初始化变量,在全局未初始化数据段,会被自动初始化为0
int global_uninit_var;

int main() {
    // 在main函数中访问全局变量
    cout << "Global Initialized Variable: " << global_init_var << endl;
    cout << "Static Global Initialized Variable: " << static_global_init_var << endl;
    cout << "Global Uninitialized Variable: " << global_uninit_var << endl;
    
    // 修改全局变量的值
    global_init_var = 30;

    // 在main函数中再次访问全局变量
    cout << "Global Initialized Variable (after modification): " << global_init_var << endl;

    system("pause");
    return 0;
}

        下面给出具体代码,使用查看变量地址的方式查看是否属于同一内存区域:

        注:全局变量和静态变量(static关键字修饰)以及常量(包含全局常量和字符串常量)都在全局区,也就是地址在全局区。局部变量和const修饰的局部变量(也就是局部常量)在非全局区

        下面这个示例程序展示了C++中的全局区和非全局区的变量存储方式。在程序中分别定义了全局变量、静态变量、常量以及局部变量和const修饰的局部变量,然后输出它们的地址。

        a、全局变量g_ag_b、静态变量static int s_as_b、全局常量c_g_ac_g_b以及字符串常量"hello world"都在全局区,这些变量的地址是固定的,它们存储的地址是相对靠近的。

        b、普通局部变量ab、const修饰的局部变量c_l_ac_l_b在非全局区,它们的地址是在栈上动态分配的,每次函数调用结束后就会被释放。

        通过输出各个变量的地址,可以看到全局区和非全局区变量的存储方式及其地址的关系。

#include<iostream>
using namespace std;


int g_a = 10;
int g_b = 10;

const int c_g_a = 10;  //全局常量
const int c_g_b = 10;

int main()
{

	// 创建普通全局变量

	int a = 10;
	int b = 10;

	cout << "局部变量a的地址为:" <<(int)&a <<endl;
	cout << "局部变量b的地址为:" << (int)&b << endl;

	cout << "全局变量g_a的地址为:" << (int)&g_a << endl;
	cout << "全局变量g_b的地址为:" << (int)&g_b << endl;

	//静态变量  在普通变量前加static 属于静态变量  它与全局变量地址比较相近
	static int s_a = 10;
	static int s_b = 10;

	cout << "静态变量s_a的地址为:" << (int)&s_a << endl;
	cout << "静态变量s_b的地址为:" << (int)&s_b << endl;


	// 常量
	// 字符串常量  "hello world"
	cout << "字符串常量hello world的地址为:" << (int)&"hello world" << endl;
	
	// const常量
	// const修饰的全局变量
    
	cout << "全局常量c_g_a的地址为:" << (int)&c_g_a << endl;
	cout << "全局常量c_g_b的地址为:" << (int)&c_g_b<< endl;
    
	// const修饰的局部变量

	const int c_l_a = 10;
	const int c_l_b = 10;

	cout << "局部常量c_l_a的地址为:" << (int)&c_l_a << endl;
	cout << "局部常量c_l_b的地址为:" << (int)&c_l_b << endl;


	system("pause");
	return 0;

}

        示例运行结果如下图所示:

03-栈区    

        栈区(Stack)是由系统自动管理的内存区域,用于存储函数调用信息和局部变量。每次函数调用时,系统会为函数分配一块栈帧,用于存储该函数的局部变量、函数参数和返回地址等信息。栈的特点包括以下几个方面:

        a、栈上的内存是有限的,栈的大小在程序启动时就已经确定,通常较小。因此,过多的递归调用或者分配过大的局部变量可能会导致栈溢出(stack overflow)问题。

        b、栈上的内存分配和释放是按照后进先出(LIFO)的原则进行的,也就是说最后分配的内存最先释放,这使得栈区非常高效。

        c、在函数调用时,局部变量会占用栈上的一定空间,并在函数退出时自动释放,无需程序员手动管理。

        下面给出示例代码,展示栈区中局部变量的使用:在这个示例中,main函数中的局部变量x和test函数中的局部变量a都是在栈上分配的。每次函数调用时,都会为函数分配一块栈帧,在其中存储局部变量和其他函数调用信息。在递归调用中,每次函数调用都会在栈上分配新的局部变量,直到达到递归结束条件才会释放栈帧。通过这个示例可以更加直观地理解栈区的特点和局部变量的生命周期。

#include <iostream>
using namespace std;

// 递归函数,调用自身多次
void test(int n) {
    int a = 10; // 在栈上分配局部变量a
    if (n > 0) {
        test(n - 1); // 递归调用
    }
}

int main() {
    int x = 5; // 在栈上分配局部变量x
    test(3); // 调用递归函数

    system("pause");
    return 0;
}

        下面给出具体代码示例,分析栈区的作用:

        注:栈区数据注意事项  --- 不要返回局部变量的地址。栈区的数据由编译器管理开辟和释放

        在下面这个示例中,函数func中定义了一个局部变量a,并返回了该局部变量的地址。在C++中,局部变量的生命周期在函数执行结束后就会结束,所以局部变量a在函数func执行完毕后就会被销毁。然而,在main函数中,通过调用func函数并将返回的地址保存在指针p中,尝试输出指针p对应地址的值。

        由于局部变量a在函数func执行结束后被销毁,再次通过指针p访问已经销毁的局部变量的地址,会导致未定义行为,这可能导致程序输出的结果是不可预测的,也就是乱码。

        在这种情况下,程序虽然能够编译通过,但会在运行时出现未定义的行为和潜在的错误。因此,应该避免返回本地局部变量的地址或使用已销毁的内存,以确保程序运行的正确性和稳定性。

#include<iostream>
using namespace std;


int * func()  // 形参数据也会放在栈区
{ 
	
	int  a = 10;  //局部变量
	return &a;  // 返回局部变量的地址,这就相当于在一个自定义函数中定义了一些局部变量,
	            // 并且带有参数,最后该函数是不能返回这个局部变量的,而且定义函数时,不能用void
}


int main()
{

	int *p = func(); // 
	// 之所以第一次可以打印正确数字,是因为编译器做了保留
	cout << *p << endl;  // 10  只写p返回的是地址,用*p使用了解引用,直接输出对应的数值,更加直观的观测出输出结果
	cout << *p << endl;  // 乱码

	system("pause");
	return 0;

}

        示例运行结果如下图所示:

04-堆区    

        堆区(Heap)是用于动态分配内存的内存区域,由程序员手动管理。在堆区上分配的内存不会在函数退出时被释放,而是需要程序员手动释放。堆区的特点包括以下几点:

        a、堆区的内存大小不固定,程序可以根据需要动态地申请或释放内存。

        b、堆上的内存分配和释放不是按照后进先出的原则,而是由程序员决定何时向操作系统申请内存和什么时候释放内存。因此,需要谨慎处理内存泄漏和内存访问错误问题。

        c、对于堆上的内存,需要程序员手动调用new来申请内存并调用delete来释放内存。

        下面给出一个示例代码,展示堆区的使用过程:在这个示例中,通过new在堆区动态分配了一个整数的内存,并将该地址保存在指针p中,然后给整数赋值并输出。最后通过delete释放了p指向的整数内存。这个示例展示了堆区的动态内存分配和释放,以及程序员手动管理堆内存的过程。要注意在使用堆内存时,需要手动管理内存的分配和释放,避免内存泄漏和悬空指针问题。

#include <iostream>
using namespace std;

int main() {
    // 动态分配内存
    int *p = new int; // 在堆区分配一个整数的内存
    
    if (p == nullptr) {
        cout << "内存分配失败" << endl;
        return 1;
    }
    
    *p = 42; // 给堆中的整数赋值
    
    cout << "动态分配的整数:" << *p << endl;
    
    // 释放内存
    delete p; // 释放p指向的整数内存
    
    system("pause");
    return 0;
}

        下面给出具体代码示例,分析堆区的作用:

        注:在堆区开辟数据。指针本质也是局部变量,放在栈上,指针保存的数据是放在了堆区。

        在下面这个示例中,func函数通过new关键字在堆区动态分配了一个整数的内存,并将该地址返回。在main函数中,通过调用func函数获取到堆区分配的整数内存的地址,并将其保存在指针p1中。然后通过解引用p1指针来输出堆区中存储的整数值。

        由于堆区中的内存是手动管理的,并且在使用new分配内存后,只有调用delete释放内存才能避免内存泄漏问题。在本例中,虽然没有手动释放分配的内存(即没有调用delete),但程序运行过程中只是输出数据,没有产生内存泄漏问题。

#include<iostream>
using namespace std;

int *func()
{
	// 利用new关键字,可以将数据开辟到堆区
	int *p =  new int(10);
	return p;

}

int main()
{

	
	int *p1 = func();  // 在这里定义了一个p1指针,它对应于开辟的堆区的数据10的地址,然后下面输出的时候,使用解引用,便可得到数值

	cout << *p1 << endl;  // 无论输出多少次,都是可以正常输出数值
	cout << *p1 << endl;

	system("pause");
	return 0;

}

         示例运行结果如下图所示:

05-new操作符    

        new操作符是C++中用于在堆区动态分配内存的操作符。它用于在堆区中分配一块指定大小的内存,并返回该内存的地址。new操作符的一般语法为:

T *ptr = new T;

        其中,T可以是任意数据类型,例如intdouble、自定义类等。通过new T,会在堆区中分配大小为T的内存,并返回一个指向该内存的指针ptr

        使用new操作符动态分配内存的好处在于,可以根据程序需要动态地分配内存,并在不需要时手动释放该内存,避免静态内存无法满足动态需求的问题。另外,使用new操作符在堆区中分配内存也可以避免函数调用结束时出现局部变量被销毁而无法再访问的问题。

        下面给出一个示例代码,展示new操作符的使用:在这个示例中,通过new操作符动态分配了一个整数内存和一个字符数组内存,并分别输出了整数和字符串。注意,当动态分配了数组内存时,需要使用delete[]来释放内存。

#include <iostream>
using namespace std;

int main() {
    // 动态分配整数内存
    int *p = new int(42); // 通过new分配一个整数并赋值为42
    cout << "动态分配的整数:" << *p << endl;

    // 动态分配字符数组内存
    char *str = new char[10]; // 通过new分配一个长度为10的字符数组内存
    strcpy(str, "Hello");
    cout << "动态分配的字符串:" << str << endl;

    // 释放动态分配的内存
    delete p; // 释放整数内存
    delete[] str; // 释放字符数组内存

    system("pause");
    return 0;
}

        下面给出具体代码示例,分析new操作符的作用:

        在下面这个示例中,定义了一个函数func,通过new关键字在堆区动态分配了一个整数内存,并将该地址返回。在main函数中,通过调用func函数获取到堆区分配的整数内存的地址,并将其保存在指针p中。然后通过解引用p指针来输出堆区中存储的整数值。

        堆区中的内存需要程序员手动管理,即在使用new分配内存后必须使用delete来释放内存。在这个示例中,在输出完堆区中的数据后,使用delete释放了p指向的堆区内存。值得注意的是,一旦内存被释放,再次访问该内存会导致未定义行为,因此在释放内存后再次解引用p指针会产生问题,所以这行代码被注释掉了。

#include<iostream>
using namespace std;

// new 的基本语法
int * func()
{
	// 在堆区创建整型数据
	// new int(10);这句代码会返回一个定义的int类型的10的地址,因此可以定义一个同类型的指针接收该地址
	int *p = new int(10);
	return p;
}

int main()
{

	int *p = func();
	cout << *p << endl;
	cout << *p << endl;
	// 堆区开辟的数据只能使用delete关键字由程序员释放,
	delete p;
//	cout << *p << endl;  // 只能输出前两个

	system("pause");
	return 0;

}

        示例运行结果如下图所示:

总结

        经过上述分析,总结如下:

        a、堆是由程序员分配和释放的内存区域,可以动态调整大小,使用new和delete管理内存。

        b、栈是由系统自动分配和释放的内存区域,用于存储函数的局部变量和函数调用信息。

        c、全局/静态存储区存储全局和静态变量,在程序运行过程中一直存在。

        d、常量存储区存储常量数据,是只读的,程序不能修改。

        正确理解和管理这四个内存区域对于编写高效、安全的C++程序至关重要。合理地利用堆、栈、全局/静态存储区和常量存储区,可以提高程序的性能并减少内存泄漏和溢出等问题的发生。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

夏目友人帐所有妖怪名单

夏目友人帐妖怪名单 夏目友人帐 第一季 2008.07.07第1话&#xff1a;猫和友人帐 / 猫と友人帐 菱垣 狞影 斑第2话&#xff1a;露神之祠 / 露神の祠 露神 濯第3话&#xff1a;八原的怪人 / 八ツ原の怪人 一只目 牛头&#xff08;中级妖怪&#xff09;第4话&#xff1a;时雨与少女…

PHP源码_众筹商城

众筹商城源码 众筹商品平台 商城加共识元富之路 网上商城众筹 前端是编译后的&#xff0c;后端PHP&#xff0c;带商城 运行截图 源码贡献 https://githubs.xyz/boot?app39 部分数据库表 CREATE TABLE ti_shopro_store (id int(11) NOT NULL AUTO_INCREMENT COMMENT ID,nam…

英语复习之英语形近词总结(二)

接着总结形近词 单词释义例句 impress 英 /ɪmˈpres/ 美 /ɪmˈpres/ vt.盖印&#xff1b;强征&#xff1b;传送&#xff1b;给予某人深刻印象 vi.给人印象。印象&#xff0c;印记&#xff1b;特征&#xff0c;痕迹 1.It didnt impress me as a good place to live. 那地方…

基于openEuler22.03 LTS环境的docker容器基础

一、说明 本文配置环境为VMware虚拟机或华为云服务器&#xff08;4核CPU&#xff0c;8 GB内存&#xff0c;40GB磁盘&#xff09;&#xff0c;OS为openEuler 22.03 LTS &#xff0c;Linux服务器要求能联网。 二、安装docker 2.1 安装docker软件包 [rootnode01 ~]# dnf -y in…

电机控制系列模块解析(15)—— 母线小电容

一、薄膜电容 在家电产品和工业变频器中&#xff0c;使用容值更小但耐压更高的薄膜电容来代替传统的电解电容作为逆变器母线电容&#xff0c;这种技术趋势已经得到了广泛应用和产品化。以下是关于这一替换技术的一些关键考量和优势&#xff1a; 长期稳定性与可靠性&#xff1a…

3-qt综合实例-贪吃蛇的游戏程序

引言&#xff1a; 如题&#xff0c;本次实践课程主要讲解贪吃蛇游戏程序。 qt贪吃蛇项目内容&#xff1a; 一、功能需求 二、界面设计 各组件使用&#xff1a; 对象名 类 说明 Widget QWidge 主窗体 btnRank QPushButton 排行榜-按钮 groupBox QGroupBox 难…

牛客NC383 主持人调度(一)【简单 排序 Java/Go/C++】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/e160b104354649b69600803184094adb 思路 直接看代码&#xff0c;不难Java代码 import java.util.*;public class Solution {/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返…

CMakeLists.txt语法规则:控制编译的变量

一. 简介 前面一篇文章学习了 CMakeLists.txt语法中的 部分常量变量&#xff0c;具体学习提供信息的变量。 本文继续学习 CMakeLists.txt语法中&#xff1a;控制编译的变量。 二. CMakeLists.txt语法规则&#xff1a;控制编译的变量 这些变量可以控制编译过程&#xff0c;…

[C语言]指针进阶详解

指针是C语言的精髓所以内容可能会比较多&#xff0c;需要我们认真学习 目录 1、字符指针 2、指针数组 3、数组指针 3.1数组指针的定义 3.2&数组名vs数组名 3.3数组指针的使用 4、数组传参和指针传参 4.1一维数组传参 4.2二维数组传参 4.3一级指针传参 4.4二级指…

【Docker学习】docker run的端口映射-p和-P选项

docker run的端口映射选项分为-p&#xff08;小写&#xff0c;全称--publish&#xff09;&#xff0c;-P&#xff08;大写&#xff0c;全称--publish-all&#xff09;&#xff0c;之前认为只有改变容器发布给宿主机的默认端口号才会进行-p的设置&#xff0c;而不改变默认端口号…

现代信号处理8_递归的最小二乘(CSDN_20240505)

递归的最小二乘大约出现在50年前。递归&#xff0c;就是在已经算出的结果的基础下&#xff0c;当新的数据到来时&#xff0c;不需要再对数据进行一次完整的运算&#xff0c;而是在已有结果的基础上做一些简单的调整&#xff0c;就能得到新的结果。使用递归的好处&#xff1a; …

面试中算法(使用栈实现队列)

使用栈来模拟一个队列&#xff0c;要求实现队列的两个基本操作:入队、出队。 栈的特点&#xff1a;先入后出&#xff0c;出入元素都是在同一端&#xff08;栈顶&#xff09;。 队列的特点&#xff1a;先入先出&#xff0c;出入元素是在两端&#xff08;队头和队尾)。 分析&…

ICode国际青少年编程竞赛- Python-1级训练场-for循环与变量

ICode国际青少年编程竞赛- Python-1级训练场-for循环与变量 1、 a 1 for i in range(4):Spaceship.step(a)Dev.step(2)Dev.step(-2)a a 12、 a 1 for i in range(4):Spaceship.step(a)Dev.step(3)Dev.step(-3)a a 13、 a 1 for i in range(4):Dev.turnLeft()Dev.step(…

【机器学习】Ctrl-Adapter:视频生成领域的革新者

Ctrl-Adapter&#xff1a;视频生成领域的革新者 一、ControlNets的挑战与Ctrl-Adapter的应运而生二、Ctrl-Adapter的技术原理与实现三、Ctrl-Adapter的应用实例与性能表现四、Ctrl-Adapter的意义与未来展望 随着人工智能技术的飞速发展&#xff0c;图像与视频生成领域正经历着前…

【电源专题】拿人体的循环系统与板级电源做个比较

一般人可能会觉得电源大概是电子设备里面比较容易搞定的门类。因为,只要线路没有接错,指示灯(如果有)能亮,电源都能工作。从这个方面说,好像是很容易。但是通过多年的经验和经历的坑,发现电源其实是一个很麻烦的东西,稍微有一点不完美就会有大问题出现。 如果将人体也当…

基于Springboot的水产养殖系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的水产养殖系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&…

ue引擎游戏开发笔记(30)——对角色移动进行优化:实现人物转向

1.需求分析&#xff1a; 当前我们只实现了通过控制器可使角色进行前后左右的移动&#xff0c;但角色移动时与动画不匹配&#xff0c;并不会进行转向&#xff0c;实现角色随移动转向。 2.操作实现&#xff1a; 1思路&#xff1a;利用反转换函数inverse transform direction获取…

GitHub Desktop安装与使用教程

GitHub Desktop 是GitHub公司推出的一款桌面应用程序&#xff0c;旨在帮助开发人员更轻松使用GitHub。它提供了一个直观的用户界面&#xff0c;允许用户通过图形化界面来执行常见的 Git 操作&#xff0c;如克隆仓库、创建分支、提交更改、合并代码等。 GitHub Desktop 的设计使…

mac自定义快捷键打开系统应用

最终效果是达成altg直接打开浏览器&#xff0c;解放双手、再也不需要移动鼠标双击打开应用啦&#xff01;&#xff01;&#xff01;&#xff5e; 1.commandspace输入自动操作 2.选择快速操作 3.选择使用工具、运行appleScrpit 4.输入打开浏览器代码 tell application "G…

Day31:单元测试、项目监控、项目部署、项目总结、常见面试题

单元测试 保证独立性。 Assert&#xff1a;断言&#xff0c;一般用来比较是否相等&#xff0c;比如 Assert.assertEquals 在JUnit测试框架中&#xff0c;BeforeClass&#xff0c;Before&#xff0c;After和AfterClass是四个常用的注解&#xff0c;它们的作用如下&#xff1a; …