说说C语言中的内存使用

C是非常贴近底层的编程语言,为了学好C语言,我们必须要要对C中的内存使用有一定程度的了解。

1 从一个实验看C的内存使用

首先编写下面的代码:

1.1 代码

#include<stdio.h>
#include<stdlib.h>

static int static_global_var;
int global_var;

void fun1()
{
    int a;
    static int b;
    printf("local var1:%p\n",&a);
    printf("local static:%p\n", &b);
}

void fun2()
{
    int a;
    printf("local var2:%p\n",&a);
}


int main()
{
    int *p;

    printf("fun1:%p\n", fun1);
    printf("fun2:%p\n", fun2);
    printf("string const:%p\n", "abc");
    printf("global:%p\n", &global_var);
    printf("static:%p\n", &static_global_var);
    fun1();
    fun2();
    p = malloc(sizeof(int));
    printf("malloc:%p\n",p);
    return 0;   
}

1.2 运行结果

在我的环境中运行结果如下:

fun1:0x40057d
fun2:0x4005b1
string const:0x400745
global:0x601054
static:0x60104c
local var1:0x7ffc9151eb4c
local static:0x601050
local var2:0x7ffc9151eb4c
malloc:0x1ae2010

%p是用于输出地址的格式符,我们用它对于上面代码中出现各种变量或者函数的地址进行了输出,下面对输出结果进行整理:

地址内容地址内容地址内容
0x40057dfun1的地址0x60104c文件内static变量0x7ffc9151eb4cfun1中的自动变量
0x4005b1fun2的地址0x601050函数内static变量0x7ffc9151eb4cfun2中的自动变量
0x400745字符串常量0x601054全局变量0x1ae2010malloc变量

1.3 分析结果

通过观察,我们可以看到:
* “指向函数的指针”和“字符串常量”被配置在非常近的内存区域;
* 函数内“static变量”、“文件内static”变量、全局变量等这些静态变量也被配置在非常近的内存区域;
* malloc()分配的变量自己在一块内存区域,离静态变量较近;
* 自动变量离上面这些内存区域非常远。

通过上面的实验可以看出来,内存区域大概分为4个部分,分别存储了常量和函数、静态变量、自动变量和malloc内存,这四个区域分别叫做

  • 只读内存区域
  • 静态变量区域

下面对这四种内存相关的内容进行说明

2 只读内存区域

如今绝大多数的操作系统都是将函数自身和字符串常量汇总配置在一个只读内存区域的。
函数本身不可能需要在运行时还做修改,所以它被配置在内存的只读区域。而对于字符串来说,一旦允许改写字符串常量,第一次调用函数输出“abc”,第二次调用函数却会输出“adc”,这种会很让人头大。

3 静态变量区域

静态变量是从程序启动到运行结束为止持续存在的变量。因此,静态变量总是在虚拟地址空间上占有固定的区域。静态变量包括:函数内的static的局部变量、文件内的static变量和全局变量,这些变量由于有效作用域不同,在编译和连接时具有不同的意义,但是在运行的时候它们都是以相似的方式被使用的

4 栈(自动变量)

通过第一部分我们可以看到,fun1和fun2两个函数中的局部变量拥有相同的地址,这是因为在声明自动变量的函数执行结束后,自动变量就不能被使用了。因此,fun1执行结束后,fun1重复使用相同的内存区域是完全没有问题的。

  • 自动变量重复使用内存区域。
  • 因此自动变量的地址是不固定的。

下面归纳了最简单的C语言函数调用实现的过程:

  1. 在调用方,参数从后往前按顺序被堆积在栈中。
  2. 和函数调用关联的返回信息(返回地址等)也被堆积在栈中。所谓的返回地址是指函数处理完毕之后应该返回的地址。正因为返回地址被堆积在栈中,所以无论函数从什么地方被调用,它都能返回到调用点的下一个处理。
  3. 跳转到作为被调用对象的函数地址。
  4. 栈为当前函数所使用的自动变量增长所需大小的内存区域。1-4所增长的栈的区域成为当前函数(被调用的函数)的可引用区域。
  5. 在函数的执行过程中,为了进行复杂的表达式运算,有时候会将计算过程中的值放在栈中。
  6. 一旦函数调用结束,局部变量占用的内存区域就被释放,并且使用返回信息返回到原来的地址。
  7. 从栈中除去调用方的参数。

5 堆(通过malloc申请的内存区域)

能够动态地(运行时)进行内存分配,并且可以通过任意的顺序释放的记忆区域被称为(heap)。

malloc()大体的实现是,从操作系统一次性地活取到比较大的内存,然后将这些内存“零售”给应用程序。根据操作系统不同,从操作系统中获取内存的手段也是不一样的,在UNIX的情况下使用brk()的系统调用。

mem

图 1 C语言使用内存的基本情况

上图展示了C语言的内存使用的基本情况,在堆的下面是一块很大的空间,系统调用brk()就是通过设定这个内存区域的末尾地址来伸缩内存空间。

调用函数的时候,栈会向地址较小的一方伸长;多次调用malloc()时,会调用一次brk(),内存区域会向地址较大的一方伸长。

malloc的具体实现的朴素原理:

内存中的一个个内存块使用类似链表的结构连接起来,各个块之前有一个管理区域,通过管理区域构建一个链表,malloc()通过遍历链表寻找空的块,如果发现尺寸合适的块,就分割出来将其变成使用中的块,并且向应用程序返回紧邻管理区域的后面区域的地址。free()将管理区域的标记写成“空块”,顺便也将上下空的块合并成一个块,这样可以防止块的碎片化。

6 内存布局对齐

假设有下面这样的一个结构体:

typedef struct {
    int int1;
    double double1;
    char char1;
    double double2;
} Hoge;

在我的环境中,sizeof(int)的结果为4,sizeof(double)的结果为8,按照正常推算Hoge的sizeof应该是:

4+8+1+8=21

但是在所有的环境中,这个答案都是不对的,在我的处理环境中答案是32,通过下面的程序,我们来看一下Hoge类型变量的各个成员的地址输出

#include<stdio.h>

typedef struct {
        int int1;
        double double1;
        char char1;
        double double2;
} Hoge;

int main(void)
{
    Hoge hoge;
    printf("hoge size.. %ld\n", sizeof(Hoge));
    printf("hoge:%p\n", &hoge);
    printf("int1:%p\n", &hoge.int1);
    printf("double1:%p\n", &hoge.double1);
    printf("char1:%p\n", &hoge.char1);
    printf("double2:%p\n", &hoge.double2);

    return 0;
}

在我的环境中,运行结果如下:

hoge size.. 32
hoge:0x7ffcc7d6dca0
int1:0x7ffcc7d6dca0
double1:0x7ffcc7d6dca8
char1:0x7ffcc7d6dcb0
double2:0x7ffcc7d6dcb8

观察运行结果可以看出来int和char后面都填充了一些空间,使得它们占用的空间都是8,这是因为根据硬件(CPU)的特征,对于不同的数据类型的可配置地址受到一定限制。此时,编译器会适当地进行边界调整(布局对齐),在结构体内插入合适的填充物。

布局对齐处理有时候也在结构体的末尾进行,这是由于有时候需要构造结构体数组的缘故。针对这样的结构使用sizeof运算符,会返回包含末尾对齐的结构体长度。将结果和元素个数相乘,就可以获得整个数组的大小。

此外,malloc()会充分考虑到各种类型的长度,返回调整后最优化的地址。局部变量等也会被配置到优化调整后的地址上。布局对齐操作是根据CPU的情况进行的,因此,根据CPU的不同,布局对齐填充的方式也不同。有些环境中,double可以被配置在4的倍数的地址上,但在很多CPU上,double智能被配置在8的倍数的地址上。

7 字节排序

在我的环境中,int存储的字节数为4,但是在这四个字节中,整数究竟是以什么样的形式存放的呢?
下面使用一段程序来验证一下。

#include<stdio.h>

int main(void)
{
    int hoge = 0x12345678;
    unsigned char *hoge_p = (unsigned char*)&hoge;

    printf("%x\n", hoge_p[0]);
    printf("%x\n", hoge_p[1]);
    printf("%x\n", hoge_p[2]);
    printf("%x\n", hoge_p[3]);

    return 0;
}

程序中将int型变量强制性赋值给unsigned char*型变量 hoge_p,因此,我们可以使用hoge_p[0]~hoge_p[3]以字节为单位引用hoge的内容。

我的环境中,程序的执行结果如下:

78
56
34
12

对于我的环境,“0x12345678”在内存中是逆向存放的。实际上Intel的CPU(包括AMD等兼容CPU)都是像这样将整数颠倒过来存放的。这种配置方式一般称为小端(little-endian)字节序。此外,对于工作站等的CPU,经常将“0x12345678”这样的值以“12,34,56,78”的顺序存放,这种配置方式称为大端(big-endian)字节序。那么,小端和大端这样的字节排列方式就称为字节排序(ByteOrder)

根据环境不同,内存中的二进制映像的形式也不尽相同,所以如果试图将内存的内容直接输出到硬盘或者通过网络进行传输以便不同的机器读取是行不通的。如果要考虑数据兼容性,建议自定义一些数据格式,然后遵循这些格式来输出数据。UNIX的XDR等工具可以在这一点为我们提供帮助。

无论是整数还是浮点小数,内存上的表现形式都随环境的不同而不同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值