Linux系统下C++程序运行时的内存布局及存储内容,生命周期,初始化时机详解。
- 1.程序代码段
- 存储内容
- 生命周期
- 初始化时机
- 特点
- 代码演示
- 2.初始化数据段 - 只读初始化数据段
- 存储内容
- 生命周期
- 初始化时机
- 特点
- 代码演示
- 3.初始化数据段 - 读写初始化数据段
- 存储内容
- 生命周期
- 初始化时机
- 特点
- 代码演示
- 4.未初始化数据段 .bss段
- 存储内容
- 生命周期
- 初始化时机
- 特点
- 代码演示
- 5.常量数据段
- 存储内容
- 生命周期
- 初始化时机
- 特点
- 代码演示
- 6. 堆区(Heap)
- 存储内容
- 生命周期
- 初始化时机
- 特点
- 代码演示
- 7. 栈区(Stack)
- 存储内容
- 生命周期
- 初始化时机
- 特点
- 代码演示
- 8.命令行参数
- 存储内容
- 生命周期
- 初始化时机
- 特点
- 代码演示
- 9.环境变量
- 存储内容
- 生命周期
- 初始化时机
- 特点
- 代码演示
在Linux系统下,一个C++程序运行时的内存布局是从低地址到高地址组织的。下面是各个内存区域的详细说明,包括它们存储的信息以及这些信息的生命周期:
1.程序代码段
存储内容
存储CPU能够执行的程序的机器指令。包括程序的自定义函数和库函数(包括静态库和动态库)编译后的机器代码。
静态库:编译时整合到可执行文件。
动态库(Linux 下的 .so 文件,Windwos 下的 .dll 文件):程序运行时被加载到内存。
生命周期
开始:程序代码段开始执行。
结束:程序代码段执行结束。
初始化时机
不涉及。
特点
程序运行时,代码段只读。
代码演示
不涉及。
2.初始化数据段 - 只读初始化数据段
存储内容
编译时初始化且运行时不需要修改的数据。
const char *
指针的指向。
const char* const
指针的指向和指针的地址。
const
修饰的全局变量。
const
修饰的命名空间作用域内的变量。
生命周期
开始:程序代码段开始执行。
结束:程序代码段执行结束。
初始化时机
程序启动时完成,main函数执行之前。
特点
只读初始化数据段通常被操作系统设置为只读。这意味着任何试图修改这一区域的数据的操作都将导致运行时错误(例如,违反访问权限的错误)。
代码演示
代码演示:
#include <iostream>
// GREETING_MESSAGE为只读的全局字符串常量,被放置在只读初始化数据段
const char* const GREETING_MESSAGE = "Hello, world!";
// DAYS_IN_WEEK为只读的全局int常量,被放置在只读初始化数据段
const int DAYS_IN_WEEK = 7;
// someConstant为只读的命名空间 someConstant 的int常量,被放置在只读初始化数据段
namespace MyNamespace
{
const int someConstant = 42;
}
int main() {
// 打印存储在全局命名空间的只读数据段的字符串常量
std::cout << GREETING_MESSAGE << std::endl;
// 打印存储在全局命名空间的只读数据段的整型常量
std::cout << "There are " << DAYS_IN_WEEK << " days in a week." << std::endl;
// 打印存储在命名空间 MyNamespace 的只读数据段整型常量
std::cout << MyNamespace::someConstant << std::endl;
return 0;
}
运行结果:
3.初始化数据段 - 读写初始化数据段
存储内容
编译时初始化且运行时可以修改的数据。
全局变量。
类的静态成员变量。
函数内的静态变量。
生命周期
开始:程序代码段开始执行。
结束:程序代码段执行结束。
初始化时机
全局变量:在程序启动时,main函数执行之前。
类的静态成员变量:在程序启动时,main函数执行之前。
函数内的静态变量:只会在第一次调用函数时初始化。
特点
这些变量可以在程序运行时被修改。
代码演示
#include <iostream>
// 全局变量,已初始化,会被存储在读写初始化数据段
int globalCounter = 0;
// 类的静态成员变量,也存储在读写初始化数据段
class Example {
public:
static int staticMember;
};
int Example::staticMember = 1;
void incrementCounter() {
// 函数内的静态变量,只会在第一次调用函数时初始化,存储在读写初始化数据段
static int counter = 0;
counter++;
std::cout << "Counter: " << counter << std::endl;
}
int main() {
// 打印全局变量和静态成员变量的初始值
std::cout << "Global counter: " << globalCounter << std::endl;
std::cout << "Static member: " << Example::staticMember << std::endl;
// 修改全局变量和静态成员变量的值
globalCounter++;
Example::staticMember++;
// 打印修改后的值
std::cout << "Global counter: " << globalCounter << std::endl;
std::cout << "Static member: " << Example::staticMember << std::endl;
incrementCounter();
return 0;
}
运行结果:
4.未初始化数据段 .bss段
存储内容
未初始化的全局变量。
未初始化的静态变量。
生命周期
开始:程序代码段开始执行。
结束:程序代码段执行结束。
初始化时机
程序的启动阶段,特别是在main函数执行之前,运行时环境会将未初始化数据段中的所有内存区域自动初始化为零或空(对于指针变量)。
特点
代码演示
#include <iostream>
// 全局变量,未初始化,默认存储在.bss段
static int global_uninit_var;
// 静态全局变量,未初始化,默认也存储在.bss段
static int static_uninit_var;
int main()
{
// 局部静态变量,未初始化,存储在.bss段
static int static_local_uninit_var;
// 打印变量的值,预期输出都为0
std::cout << "global_uninit_var:" << global_uninit_var << std::endl;
std::cout << "static_uninit_var:" << static_uninit_var << std::endl;
std::cout << "static_local_uninit_var:" << static_local_uninit_var << std::endl;
return 0;
}
运行结果:
在上面的代码中:
global_uninit_var
是一个全局变量,它没有被显式初始化,所以默认值是0。
static_uninit_var
是一个静态全局变量,同样没有被初始化,其默认值也是0。
static_local_uninit_var
是一个静态局部变量,在函数内部声明,没有被初始化,因此它也将被存储在.bss段,并且其默认值是0。
当你编译并运行这个程序时,这些变量都会被置为0。这是因为它们都是在.bss段中并且在程序启动阶段被自动清零。这种行为确保了未初始化的静态和全局变量有一个确定的起始值。
5.常量数据段
存储内容
程序中的常量。
生命周期
开始:程序代码段开始执行。
结束:程序代码段执行结束。
初始化时机
在程序加载到内存并开始运行之前,由操作系统完成的。
特点
代码演示
#include <iostream>
int main() {
// Hello, World! 字符串字面量存储在常量数据段中
const char* hello = "Hello, World!";
// 打印存储在常量数据段的字符串
std::cout << hello << std::endl;
return 0;
}
运行结果:
6. 堆区(Heap)
存储内容
动态内存分配的对象。
生命周期
开始:通过动态内存分配函数(如C++中的 new
或C中的 malloc
)创建。
结束:通过对应的内存释放函数(如C++中的 delete
或C中的 free
)显式释放。
初始化时机
初始化时机取决于具体的分配和初始化方式。
使用new操作符的时候,可以同时进行分配和初始化。
使用malloc等C语言风格的内存分配时,分配和初始化是分开的,初始化需要显式进行。
特点
堆向高地址增长(即“向上”增长)。
代码演示
#include <iostream>
class Sample {
public:
int value;
Sample(int v) : value(v) {} // 构造函数
void display() { std::cout << "Value: " << value << std::endl; }
};
int main() {
// 在堆上动态分配一个Sample对象
Sample* samplePtr = new Sample(10); // 分配并初始化
// 使用对象
samplePtr->display(); // 显示 "Value: 10"
// 释放分配的内存
delete samplePtr; // 结束对象的生命周期
return 0;
}
运行结果:
7. 栈区(Stack)
存储内容
局部变量。
函数参数。
返回地址。
控制流信息等。
生命周期
开始:当声明一个局部变量或进入一个函数时。
结束:当离开变量的作用域或函数返回时。
初始化时机
局部变量在声明时可以进行初始化。
函数参数在函数调用时,通过传递实参来初始化。
特点
栈通常向低地址增长(即“向下”增长)。
代码演示
#include <iostream>
void function() {
int localVar = 5; // 局部变量的声明和初始化
std::cout << "Local variable in function: " << localVar << std::endl;
// 离开函数时,localVar的生命周期结束
}
int main() {
function(); // 调用function,localVar在这里开始生命周期
// 在这里,function中的localVar不再存在,已经被自动销毁
return 0;
}
运行结果:
8.命令行参数
存储内容
argc(Argument Count): 一个整数,表示传递给程序的命令行参数数量,包括程序名本身。
argv(Argument Vector): 一个字符串数组,存储具体的参数值。argv[0]是程序名,argv[1]至argv[argc-1]是用户传递给程序的参数。
生命周期
开始:程序启动时,操作系统准备命令行参数。
结束:程序终止时,命令行参数的生命周期结束。
初始化时机
命令行参数在程序启动之前由操作系统解析和准备,然后在程序的main函数启动时通过argc和argv参数传递给程序。
特点
命令行参数是在程序启动时由操作系统传递给程序的参数,这些参数允许用户指定程序运行时的行为或输入。在C和C++中,这些参数通过main函数的参数接收,通常是两个参数:argc(参数计数)和argv(参数向量)。
代码演示
#include <iostream>
int main(int argc, char *argv[]) {
std::cout << "You have entered " << argc << " arguments:" << std::endl;
for(int i = 0; i < argc; ++i) {
std::cout << argv[i] << std::endl;
}
return 0;
}
这个程序会输出用户输入的所有命令行参数,包括程序名本身。假设程序名为program,并且运行时输入了两个额外的参数arg1 arg2,那么输出将会是:
You have entered 3 arguments:
program
arg1
arg2
9.环境变量
存储内容
键值对形式的字符串,其中键是变量名,值是设置的值。
生命周期
开始:在计算机启动或用户登录时由操作系统初始化。
结束:在计算机关闭或用户登出时结束。
初始化时机
环境变量在程序启动前由操作系统初始化,对所有的程序和进程都是可见的。
特点
环境变量是操作系统中用于存储系统级或用户级配置信息的全局变量。它们由操作系统或用户设置,并在程序启动前就已经存在。
代码演示
#include <iostream>
#include <cstdlib> // 提供getenv
int main() {
// 获取名为"PATH"的环境变量
const char* path = getenv("PATH");
if (path != nullptr) {
std::cout << "PATH: " << path << std::endl;
} else {
std::cout << "PATH environment variable does not exist." << std::endl;
}
return 0;
}
这段代码中,getenv函数尝试获取名为"PATH"的环境变量的值。如果该环境变量存在,它会返回指向相应值的指针;如果不存在,返回nullptr。
环境变量对程序是只读的;尽管程序可以修改自己的环境变量副本,但这些修改不会影响到其他程序或操作系统级别的设置。