c程序的内存映像

c语言当中数据存储的内存空间是有区分的 (我自己分的不准确,差不多理解这个意思就行,而且这个问题实际是由操作系统具体实现决定的)

程序代码和只读数据(部分常量)存放在 代码段 当中
程序的静态数据存放在 静态数据段 当中
程序的动态数据存放在 动态数据段(栈/堆) 当中

  • 一般用户进程空间(低->高)从一小段保留地址,接下来就是代码段(.text),存放的是编译好的机器码
  • 静态存储区分配 全局变量、静态变量、常量
    • 数据段(.data)、常量段(.rodata)、未初始化的全部和静态变量段(.bss)
  • 在堆(heap)上存放用户申请的内存(这部分空间容量比栈大一个量级(GB和MB),所以程序中实际数据对象最好在这里分配),堆一般向上生长
    在程序运行期间,用 动态内存分配函数 来申请的内存都是从堆上分配的,动态内存的生存周期由人为的进行决定
  • 在栈(stack)上存放的是 函数参数,局部变量值等,栈一般向下增长,当栈堆指针相等时就说明内存空间必然是不够了
    在执行函数调用时,系统在栈上为函数的局部变量和形参分配内存,函数执行结束时,自动释放这些内存
  • 此外,实际上中间堆栈中间可能还存在动态共享库
  • 最后在栈之后是用户进程空间中执行内核程序的保留空间

内存分配函数

尽管动态存储分配函数需要为不同对象分配存储空间,但显然一般程序中只会设定一个内存分配函数,但是假定用一个分配程序来处理多种类型的请求,如:

指向char类型的指针和指向struct tnode类型的指针

此时则存在两个问题

  • 如何在大多数机器中满足各种类型对象的对其要求
    • 如:整型对象通常必须分配在偶数地址上
  • 使用什么样的声明能处理分配程序必须能返回不同类型指针的问题

对齐要求一般只需要确保分配程序始终满足所有对齐限制要求的指针即可,其代价是牺牲一些空间,对于任何执行严格类型检查的语言来说,像malloc()这样的函数的类型声明总是很令人头疼的问题。在c语言中,一种合适的方法是将返回值类型声明为void*,然后再显式地将该指针强制转换为所需类型,malloc并不是一个在编译时就确定的固定大小的数组中分配空间,而是在需要时向操作系统申请空间。

由于程序中某些地方可能并不通过malloc()调用申请空间(通过其他方式申请空间),因此通过malloc()可以申请的空闲空间不一定是连续的,而这些malloc()管理的空闲存储空间以空闲块链表的形式组织,每个块包含一个长度、一个指向下一块的指针以及一个指向自身存储空间的指针。

  • 这些空闲块按照存储地址的升序组织,最后一块指向第一块(循环链表),当有申请请求时,malloc()扫描空闲块链表,直到找到一个足够大的空闲块为止,该算法称为“首次适应”(first fit)- 与之相对的算法是“最佳适应”(best fit),它寻找满足条件的最小空闲块,如果找到的空闲块恰好与请求的大小相符合,则将它从链表中移走并返回给用户,如果该块太大,则将它分成两半:大小合适的块返回给用户,剩下的块留在空闲块链表中。

如果找不到一个足够大的空闲块,则向操作系统申请一个大块加入到空闲块链表中,具体实现是通过sbrk、brk等系统调用,UNIX系统调用sbrk返回一个指针,该指针指向n个字节的存储空间。尽管如果没有存储空间时返回NULL更恰当,但sbrk系统调用返回的是-1,因此必须将-1转换为char*类型,以便于返回值进行比较,此处进行强制类型转换的另一个好处是不会受不同机器中指针表示不同的影响,但仍有一个前提是其返回的多个指针之间可以进行有意义的比较,而ANSI标准中没有保证这一点,它只允许指向同一个数组的指针之间的比较。

最后的释放过程也是首先搜索空闲块链表,以找到可以插入被释放块的合适位置,将被释放块插入到空闲块中,如果与被释放块相邻的任一边是一个空闲块,则将两个块合成一个大空闲块,以使存储空间不会有太多的碎片,由于空闲块链表是以地质递增顺序链接在一起的,所以很容易判别相邻的块是否空闲(通过链表指针),malloc()返回的存储空间需要满足将要保存对象的对齐要求。虽然机器类型各异,但每个机器都有一个最受限的类型:如果最受限的类型可以存储在某个特定地址中,则其他所有类型都可以存放在此地址中。在某些机器中,最受限的类型是double类型,而在另外一些机器中,最受限的是int或long类型,位于空闲块开始处的控制信息称为“头部”。为了简化块的对齐,所有块的大小都必须是头部大小的整数倍,且头部已正确地对齐,这通过一个联合实现,该联合包含所需的头部结构以及一个对齐要求最受限的类型的实例,如:

typedef long Align;             //按long类型的边界对齐(假定long为最受限的类型)
union header{                   //块的头部
    struct{
        union header*ptr;       //空闲块链表中的下一块(供下次申请时找到空闲块)
        unsigned size;          //本块的大小,空闲块被要求是头部长度的整倍数,而size就是这个倍数
    }s;
    Align x;                    //强制块的对齐
};
typedef union header Header;

该联合中,Align永远不会被使用,它仅仅用于强制每个头部在最坏情况下满足对其要求,在malloc()中,请求的长度(以字符为单位)将被舍入,以保证它是头部大小的整数倍。实际分配的块(请求到的块)将多包含一个单元,用于头部本身,块的大小将被记录在头部的size字段中。malloc()返回的指针将指向空闲空间(头部的下一个可供使用的空闲单元),而不是块的头部,用户可对获得的存储空间进行任何操作,但是如果在分配的存储空间之外写入数据则可能会破坏链表(写在未被分配的空闲块中),头部中的size字段是必须的,因为由malloc()控制的块不一定是连续的,所以无法通过指针算数运算计算每个块的大小。

动态内存分配函数

需包含头文件<stdib.h>,malloc()和calloc()函数原型如下:

void* malloc(size_t n);
当分配成功时,返回一个指针,该指针指向n字节长度的未初始化的存储空间,若申请不成功则返回NULL

void* calloc(size_t n,size_t size);
当分配成功时,返回一个指针,该指针指向的空闲空间足以容纳由n个指定长度的对象组成的数组,若申请不成功则返回NULL,该存储空间被初始化为0

size表示申请的字节数,如果不清楚要申请的具体字节数可以用sizeof(要申请的变量类型)来输入size,根据请求的对象类型,malloc()或calloc()返回的指针满足正确的对齐要求。
void*类型可以强制转换为任何其它类型的指针,因此malloc()通常需要根据申请的变量空间类型强转相应的指针(Type*),如:

p=(int*)malloc(n*sizeof(int));if(p==NULL)printf(“内存分配失败”);
for(i=0;i<n;i++){*(p+i)=i+1;printf("%d",*(p+i));} *p为分配的10个整型空间的首地址,依次向10个空间输入数据并输出

在c语言的部分编译环境下void*可以为其他类型指针赋值,因此有时不进行类型转换也是可以的,如:

p=malloc(n*sizeof(int));(但是在c++中使用malloc函数必须进行强制类型转换)

动态内存释放函数

malloc()和calloc()申请的内存块都由free()函数进行释放,原型如下:

void free(void* p); p只能为malloc()或calloc()返回的指针,不能是其他的任何变量
free时系统标记此内存为未占用,可被重新分配

需要注意的是,此时p指向的空间没有变,但动态内存空间已被系统回收,如果再用*p对此块内存空间作修改,可能会出现很多错误,如:

for(p=head;p;p=p->next)free§;是一个非常典型的错误代码段,p在释放后是无法找到p->next的

正确处理方法是在释放项目以前将一切必要的信息保存起来

for(p=head;p;p=q)q=p->next,free§;

因此在释放空间后通常将p设置为空指针(p=NULL)。存储空间的释放顺序没有限制,malloc()和calloc()尽量一起使用,防止出现内存空间分配不足等问题。不用的内存空间要么释放,要么用指针变量保存起来,以便之后使用时可以找得到此处内存空间。

常见的内存错误和分配原则

  • 内存分配未成功就开始使用
  • 内存分配成功但未进行初始化就开始使用
  • 内存分配成功并初始化,但发生越界错误
  • 申请内存后没有直接释放内存
  • 释放内存后仍继续使用

关于内存分配编程的建议原则:

  • 尽在需要时才使用动态分配函数
  • malloc()和free()要配对使用,malloc()在函数入口,free()在函数出口
  • 使用malloc()时要检查函数返回值
  • 使用free()后将指针设置为NULL(防止多个指针指向同一块区域,对同一块内存空间多次释放)
  • 不要把局部变量的地址作为函数返回值返回

动态内存示例程序:

#include<stdio.h>
#include<stdlib.h>
int main()
{
    const int n=10;
    int i;
    int *p,*q;                        //q指针用于记录分配空间并最后释放空间。对分配空间内的操作通过p指针完成
    p=(int*)malloc(n*sizeof(int));     //分配空间
    q=p;
    if(p)
    {
        for(i=0;i<n;i++,p++)           //如果取到n以后的数,则超过了系统给p分配的空间,会发生越界错误
        {
            *p=i+1;                    //必须分配空间并初始化后才能输出,否则输出一个随机数

            printf("%d",*p);
        }
    }
    free(q);                           //如果此处直接free(p),此时p已经指向了i=n+1的地址空间,超过了系统分配空间的范围,因此也会发生越界错误
    q=NULL;                            //释放内存后,将q置为空指针
    return 0;
}