理解一个运行中的程序在内存中的存储方式,对编写高质量代码有很大的帮助,在编程过程中,依据实际需求选择合适的内存区域,会使得代码的结构简洁高效。
可执行文件是一种特殊的文件格式,包含了计算机可以直接运行的程序代码和相关数据。不同的操作系统使用不同的可执行文件格式,例如 Windows 上的 PE(Portable Executable)格式、Linux/Unix 上的 ELF(Executable and Linkable Format)格式,以及 macOS 上的 Mach-O 格式。
当一个可执行文件被操作系统加载到内存中时,通常会被分配到一个内存空间中。这个空间被划分为多个区域,每个区域有特定的用途。以下是典型的内存空间布局(从低地址到高地址):
-
代码段(Text Segment)
-
用途:存储可执行文件的机器指令(程序代码)。
-
特点:通常是只读的,防止代码被意外修改。
-
映射来源:对应可执行文件中的 .text 节(或类似节)。
-
-
数据段(Data Segment)
-
用途:存储程序的已初始化全局变量和静态变量。
-
特点:可读可写。
-
映射来源:对应文件中的 .data 节。
-
-
BSS 段(BSS Segment)
-
用途:存储未初始化的全局变量和静态变量(默认初始化为 0)。
-
特点:可读可写,但通常不在文件中占用空间,只在内存中分配。
-
映射来源:由操作系统根据文件元数据分配,不直接存储在可执行文件中。
-
-
堆(Heap)
-
用途:动态内存分配区域,用于程序运行时通过 malloc 或 new 等函数分配内存。
-
特点:大小动态增长,从低地址向高地址扩展。
-
管理:由运行时库和操作系统共同管理。
-
-
栈(Stack)
-
用途:存储函数调用时的局部变量、参数和返回地址。
-
特点:大小有限,从高地址向低地址增长(与堆相反)。
-
管理:由操作系统分配初始栈空间,程序运行时动态调整。
-
-
环境变量和参数区
-
用途:存储程序启动时的环境变量(如 PATH)和命令行参数。
-
特点:位于高地址区域,通常靠近栈顶部。
-
-
内核空间(Kernel Space)
-
用途:操作系统内核代码和数据的区域,普通程序无法直接访问。
-
特点:位于虚拟内存的最高地址段,受保护隔离。
-
为了便于理解程序的内存空间,一般情况下从四个角度来看内存空间:动态存储区、静态存储区、堆和栈。
一、动态存储区 与 静态存储区
动态存储区 和 静态存储区 的主要区别在于内存空间的分配方式。
1、静态存储区是在程序编译时就分配好内存空间的区域。它主要包括全局变量、静态变量和常量,这些变量在程序的生命周期内一直存在。静态存储区的空间大小是在编译时确定的,不能动态地扩展或缩小。
2、动态存储区是在程序运行时动态分配内存空间的区域。它主要用于存储局部变量、函数参数等,这些变量在程序运行时才被创建,在使用完后被销毁。动态存储区的空间大小是可以动态扩展和缩小的,通常由操作系统进行管理。
二、堆(Heap) 与 栈(Stack)
堆和栈是两种不同的内存分配机制,它们的区别主要体现在以下几个方面:
1、内存分配方式
堆 是动态分配内存的区域,堆(Heap)中的地址是由低到高的,通常由程序员手动分配和释放内存,需要显式地请求操作系统分配内存,并在使用完后自行释放。
栈(Stack)是自动分配内存的区域,它在函数调用时自动为局部变量和函数参数分配内存空间。当函数返回时自动释放,无需程序员手动干预。
2、存储内容
堆 通常用于存储全局变量、动态分配的内存块和一些需要长时间保存的数据。由于堆内存的分配是动态的,因此它可以根据需要灵活地扩展或缩小。
栈(Stack)主要用于存储局部变量、函数参数等短期数据。这些数据在函数返回后就不再需要,因此栈内存会在函数返回时自动释放。
3、操作方法
堆 的内存分配和释放需要程序员显式地操作。程序员需要在程序中申请内存(malloc 函数),并在使用完后手动( free 函数)释放。这些操作需要仔细的规划和管理,以避免内存泄漏和其他问题。如果没有被人工释放,程序结束时由OS回收。堆中数据生命周期等同于程序的声明周期。
栈(Stack)的内存地址是由高到低的。其在内存中的分配和释放由 操作系统 进行管理,在函数返回时由操作系统自动释放。程序员只需要定义局部变量和函数参数,并。因此,栈内存的操作相对简单,无需过多的程序员干预。
栈(Stack)按分配方式分为两种:静态栈和动态栈;
A、静态栈:由编译器分配完成,如局部变量
B、动态栈:由alloca()函数进行分配,由编译器进行释放。
三、动态存储区、静态存储区、堆和栈
1、动态存储区
动态存储区主要用于动态分配内存,可以在程序运行时根据需要扩展或缩小。因此,当需要在程序运行期间动态地创建和销毁数据时,通常使用动态存储区。例如,当需要实现字符串动态分配内存时,可以使用动态存储区。
2、 静态存储区
静态存储区主要用于全局变量、静态变量和常量的存储。这些变量在整个程序的生命周期内一直存在。因此,当需要声明全局变量或常量时,通常使用静态存储区。如在程序中,全局变量和常量通常定义在全局作用域中,使用静态存储区。
3、堆
堆主要用于长期数据的存储以及动态内存分配。由于堆内存的分配和释放需要手动操作,因此在使用堆时需要仔细规划和管理内存。例如,当需要实现动态数组或自定义数据结构时,可以使用堆内存。
4、栈
栈主要用于短期数据的存储以及函数调用时的局部变量和参数的分配。由于栈内存的分配和释放是自动进行的,因此在使用栈时无需过多干预。例如,在函数调用时,可以使用栈来存储局部变量和参数。当函数返回时,栈内存会自动释放,避免手动释放内存的麻烦。
四、 static 与 变量及函数
静态变量的类型说明符是static。 静态变量属于静态存储方式,但是属于静态存储方式的量不一定就是静态变量。 如外部变量虽属于静态 存储方式,但不一定是静态变量,必须由 static 加以定义后才能成为静态外部变量,或称静态全局变量。
1、静态局部变量
静态局部变量属于静态存储方式,它具有以下特点:
(1)静态局部变量在函数内定义 它的生存期为整个源程序,但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。
(2)允许对构造类静态局部量赋初值 例如数组,若未赋以初值,则由系统自动赋以0值。
(3)对基本类型的静态局部变量若在说明时未赋以初值,则系统自动赋予0值。而对自动变量不赋初值,则其值是不定的。 根据静态局部变量的特点, 可以 看出它是一种生存期为整个源程序的量。虽然离开定义它的函数后不能使用,但如再次调用定义它的函数时,它又可继续使用, 而且保存了前次被调用后留下的 值。 因此,当多次调用一个函数且要求在调用之间保留某些变量的值时,可考虑采用静态局部变量。虽然用全局变量也可以达到上述目的,但全局变量有时会造成 意外的副作用,因此仍以采用局部静态变量为宜。
2、静态全局变量
全局变量(外部变量)的说明之前再冠以static 就构 成了静态的全局变量。全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。 这两者在存储方式上并无不同。这两者的区别虽在于非静态全局 变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而静态全局变量则限制了其作用域, 即只在 定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用, 因此 可以避免在其它源文件中引起错误。从以上分析可以看出, 把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量 后是改变了它的作用域, 限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的。
3、static 函数
关于内部函数和外部函数。
当一个源程序由多个源文件组成时,C语言根据函数能否被其它源文件中的函数调用,将函数分为内部函数和外部函数。
(1) 内部函数(又称静态函数)
如果在一个源文件中定义的函数,只能被本文件中的函数调用,而不能被同一程序其它文件中的函数调用,这种函数称为内部函数。
定义一个内部函数,只需在函数类型前再加一个“static”关键字即可,如下所示:
static 函数类型 函数名(函数参数表)
{……}
关键字“static”,译成中文就是“静态的”,所以内部函数又称静态函数。但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件。
使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名,因为同名也没有关系。
(2) 外部函数
外部函数的定义:在定义函数时,如果没有加关键字“static”,或冠以关键字“extern”,表示此函数是外部函数:
[extern] 函数类型 函数名(函数参数表)
{……}
调用外部函数时,需要对其进行说明:
[extern] 函数类型 函数名(参数类型表)[,函数名2(参数类型表2)……];
五、const
用来修饰某个数,某个函数,某个指针,表明其值是只读的,不能被赋值(这是相对的说法,通过指针操作可以仍可突破这个限制)。
1、修饰某个数
const int n=1; n 是一个只读变量,程序不可以直接修改其值。这里还有一个问题需要注意,即如下使用:int a[n];在ANSI C中,这种写法是错误的,因为数组的大小应该是个常量,而n只是一个变量。
2、修饰某个指针
const int *p;表示指针所指向的值不能修改
int const *p;表示指针所指向的值不能修改
int * const p;表示这个指针不可修改,即指针的指向不能修改
六、面向对象的数据
1、静态数据成员
在类内数据成员的声明前加上static关键字,该数据成员就是类内的静态数据成员。
静态数据成员存储在全局数据区,静态数据成员在定义时分配存储空间,所以不能在类声明中定义静态数据成员(仅仅是声明一下),无论定义了多少个类的对象,静态数据成员为该类所对象共有,对该类的所有对象可见。就是说任何该类对象都可以对静态数据成员进行操作。静态数据成员只属于该类,不属于任何对象,在没有类的实例时其作用域就可见,在没有任何对象时,就可以进行操作,与普通数据成员一样,静态数据成员也遵从public, protected, private访问规则。
静态数据成员的初始化: <数据类型><类名>::<静态数据成员名>=<值>
静态数据成员访问方式: <类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>
与全局变量相比较,使用静态数据成员有两个优势,一是静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性。二是静态数据成员可以是private成员,而全局变量不能成为private成员。
2、静态成员函数。
与静态数据成员类似,静态成员函数属于整个类,而不是某一个对象。
静态成员函数没有this指针,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数,出现在类体外的函数定义不能指定关键字static,非静态成员函数可以任意地访问静态成员函数和静态数据成员。