C++程序在执行时,将内存大方向划分为4个区域
程序运行前:
-
代码区:存放函数体的二进制代码,由操作系统进行管理
-
全局区:存放全局变量和静态变量以及常量
程序运行后:
-
栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
-
堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
内存四区意义:
不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程
1.1 程序运行前
在程序编译后,生成exe可执行程序。未执行程序前,只有两个区在工作。
代码区:
存放CPU执行的机器指令(0/1机器码)
代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
代码区是只读的,目的是防止程序以外的修改了代码区的指令
全局区:
全局变量和静态变量存放在此
全局变量还包含了常量区,字符串常量和其他常量( const 修饰的变量)也存放在此
该区域的数据在程序结束后由操作系统释放
#include<iostream>
using namespace std;
int main()
{
//全局区
//全局变量/静态变量/常量
//创建普通局部变量
int a = 10;
int b = 10;
cout << "局部变量a的地址为:" << (int)&a << endl;
cout << "局部变量b的地址为:" << (int)&b << endl;
return 0;
}
编译报错:
------------------------------------------------------------------------------
全局区.cpp: In function ‘int main()’:
全局区.cpp:14:37: error: cast from ‘int*’ to ‘int’ loses precision [-fpermissive]
14 | cout << "局部变量a的地址为:" << (int)&a << endl;
| ^~~~~~~
全局区.cpp:15:37: error: cast from ‘int*’ to ‘int’ loses precision [-fpermissive]
15 | cout << "局部变量b的地址为:" << (int)&b << endl;
| ^~~~~~~
报错是因为尝试将一个整数指针(int*)转换成一个整数(int)。在32位系统上,这种转换通常可以正常工作,因为指针和整数通常都是32位的。但在64位系统上,指针是64位的,而int通常是32位的。因此,当你尝试将一个64位指针转换为32位整数时,编译器警告你这种转换会丢失精度。
解决这个问题的一种方法是将指针转换为uintptr_t类型,这是一个无符号整型,其大小足以存放指针,无论在32位还是64位系统上。请记得包含头文件 <cstdint> 来使用uintptr_t类型。这样的转换能够保持地址的完整性,避免精度丢失的问题。
改成下面的样式即可:
#include <iostream>
#include <cstdint> // 添加这行来使用 uintptr_t
using namespace std;
//全局变量,定义在函数外面
int g_a = 10;
int g_b = 10;
//const修饰的全局变量,即全局常量
const int c_g_a = 10;
const int c_g_b = 10;
int main() {
//普通局部变量,定义在main函数里面
int a, b;
cout << "局部变量a的地址为:" << static_cast<uintptr_t>(reinterpret_cast<uintptr_t>(&a)) << endl;
cout << "局部变量b的地址为:" << static_cast<uintptr_t>(reinterpret_cast<uintptr_t>(&b)) << endl;
cout << "全局变量a的地址为:" << static_cast<uintptr_t>(reinterpret_cast<uintptr_t>(&g_a)) << endl;
cout << "全局变量a的地址为:" << static_cast<uintptr_t>(reinterpret_cast<uintptr_t>(&g_b)) << endl;
//静态变量,在普通局部变量前加static,在内存中的位置邻近全局变量
static int s_a = 10;
static int s_b = 10;
cout << "静态变量a的地址为:" << static_cast<uintptr_t>(reinterpret_cast<uintptr_t>(&s_a)) << endl;
cout << "静态变量a的地址为:" << static_cast<uintptr_t>(reinterpret_cast<uintptr_t>(&s_b)) << endl;
//常量
//字符串常量
cout << "字符串常量的地址为:" << static_cast<uintptr_t>(reinterpret_cast<uintptr_t>("Hello World")) << endl;
//const修饰的常量
//const修饰的全局常量
cout << "const修饰的全局常量c_g_a的地址为:" << static_cast<uintptr_t>(reinterpret_cast<uintptr_t>(&c_g_a)) << endl;
cout << "const修饰的全局常量c_g_b的地址为:" << static_cast<uintptr_t>(reinterpret_cast<uintptr_t>(&c_g_b)) << endl;
//const修饰的局部常量
const int c_l_a = 10; // c-const g-gloabal l-local
const int c_l_b = 10;
cout << "const修饰的局部常量c_l_b的地址为:" << static_cast<uintptr_t>(reinterpret_cast<uintptr_t>(&c_l_b)) << endl;
cout << "const修饰的局部常量c_l_b的地址为:" << static_cast<uintptr_t>(reinterpret_cast<uintptr_t>(&c_l_b)) << endl;
return 0;
}
输出:
-----------------------------------------------------------------------------------
局部变量a的地址为:140723000827512
局部变量b的地址为:140723000827516
全局变量a的地址为:110713891356688
全局变量a的地址为:110713891356692
静态变量a的地址为:110713891356696
静态变量a的地址为:110713891356700
字符串常量的地址为:110713891348633
const修饰的全局常量c_g_a的地址为:110713891348488
const修饰的全局常量c_g_b的地址为:110713891348492
const修饰的局部常量c_l_b的地址为:140723000827520
const修饰的局部常量c_l_b的地址为:140723000827520
总结:
-
C++中在程序运行前分为全局区和代码区
-
代码区特点是共享和只读
-
全局区中存放全局变量/静态变量/常量
-
常量区(全局区的一部分)中存放 const 修饰的全局常量和字符串常量
1.2 程序运行后
栈区:
由编译器自动分配释放,存放函数的参数值(形参),局部变量等
注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
#include<iostream>
using namespace std;
//栈区数据注意事项
//1.不要返回局部变量的地址
//2.栈区的数据由编译器管理开辟和释放
int* func(int b) //形参b也会放在栈区
{
int a = 10; //局部变量,存放在栈区,栈区的数据在函数执行完后自动释放
return &a; //返回局部变量的地址
}
int main()
{
int * p = func(1);
cout << *p << endl;
return 0;
}
编译器报错:
------------------------------------------------------------------------------
栈区.cpp: In function ‘int* func()’:
栈区.cpp:12:12: warning: address of local variable ‘a’ returned [-Wreturn-local-addr]
12 | return &a; //返回局部变量的地址
| ^~
栈区.cpp:11:9: note: declared here
11 | int a = 10; //局部变量,存放在栈区,栈区的数据在函数执行完后自动释放
| ^
这个警告消息是编译器在告诉你有潜在的危险操作:你正在尝试从一个函数中返回一个局部变量的地址。局部变量是存储在函数调用栈上的,这意味着当函数执行完毕并返回后,局部变量的内存空间将被释放回操作系统,并可能被后续的函数调用或其他数据覆盖。因此,局部变量的地址在函数外部是无效的,任何尝试访问该地址的操作都是不确定的,通常会导致不稳定的行为或程序崩溃。
解决这个问题的方法通常有以下几种:
-
使用全局变量或静态变量:这些变量的生命周期覆盖了程序的整个运行时期,因此返回它们的地址是安全的。但请注意,过度使用全局变量会使程序的结构变得混乱,不易维护。
-
动态分配内存:使用动态内存分配(例如,C++中的new或C中的malloc)来创建对象或数据。动态分配的内存位于堆区,它的生命周期直到使用delete(C++)或free(C)显式释放之前都有效。这意味着你可以安全地返回指向这些内存的指针,但必须记得最终释放它以避免内存泄漏。
-
使用引用或指针传递:如果可能,通过引用或指针传递变量到函数外部,而不是返回指针或引用。这样,数据的管理(创建和销毁)可以在调用函数外部进行,从而避免了从函数内部返回局部数据的问题。
-
返回值:如果函数的目的只是要返回某种计算的结果,最简单的办法可能就是直接返回计算结果的值,而不是返回指向该结果的指针或引用。
下面是一个修正示例,如果你的目标是返回一个整数值:
int func() {
int a = 10; // 局部变量
return a; // 直接返回值
}
堆区:
由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
在C++中主要利用 new 在堆区开辟内存
#include<iostream>
using namespace std;
int* func()
{
//利用 new 关键字,可以将数据开辟到堆区,并返回数据对应的地址
//指针本质也是局部变量,放在栈上,但指针保存的数据放在堆区
int * p = new int(10);
return p;
}
int main()
{
//在堆区开辟数据
int * p = func();
cout << *p << endl;
return 0;
}
输出:
-----------------------------------------------------------------------------------
10
1.3 new操作符
C++中利用new操作符在堆区开辟数据
堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符delete
语法:new 数据类型
利用new创建的数据,会返回该数据对应的类型的指针(即数据对应的地址)
#include<iostream>
using namespace std;
//1.new的基本语法
int* func()
{
//在堆区创建整形数据
//new返回的是对应数据类型的指针
int * p = new int(10);
return p;
}
void test01()
{
//堆区的数据,由程序员管理开辟和释放
int * p = func();
cout << *p << endl;
cout << *p << endl;
cout << *p << endl;
//释放堆区数据
delete p;
// cout << *p << endl; //内存已经被释放,再次访问就是非法操作,会报错
}
//2.在堆区利用new开辟数组
void test02()
{
//在堆区创建包含10个整形数据的数组
int * arr = new int[10];
for (int i = 0; i < 10; i++)
{
arr[i] = i + 100; //给数组元素赋值,100~109
}
for (int i = 0; i < 10; i++)
{
cout << arr[i] << endl;
}
//释放堆区数组
delete[] arr;
}
int main()
{
test01();
test02();
return 0;
}
输出:
----------------------------------------------------------------------------------------------------
10
10
10
100
101
102
103
104
105
106
107
108
109