前言
当程序运行时,操作系统会为程序分配一块内存空间,这块内存空间被划分为不同的区域,每个区域有其独特的作用和管理方式。四个区域分别为:堆、栈、全局/静态存储区和常量存储区。每个区域都有不同的作用和特点,下面分别进行详细介绍。
正文
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_var
和static_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_a
和g_b
、静态变量static int s_a
和s_b
、全局常量c_g_a
和c_g_b
以及字符串常量"hello world"
都在全局区,这些变量的地址是固定的,它们存储的地址是相对靠近的。
b、普通局部变量a
和b
、const修饰的局部变量c_l_a
和c_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
可以是任意数据类型,例如int
、double
、自定义类等。通过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++程序至关重要。合理地利用堆、栈、全局/静态存储区和常量存储区,可以提高程序的性能并减少内存泄漏和溢出等问题的发生。