《征服C指针》——读书笔记(4)

来源:互联网 发布:消防工程师 知乎 编辑:程序博客网 时间:2024/06/02 00:31

一、函数与字符串常量

   1. 只读内存区域

       如今的大多数操作系统都是将函数自身和字符串常量汇总配置在一个只读内存区域的。

       函数本身一旦被写定后基本不再需要改写,所以它被配置在内存的只读区域。此外,如果执行程序是只读的,在同一份程序被同时启动多次的时候,通过在物理地址上共享程序能够节约物理内存。由于硬盘上已经存放了可执行程序,就算内存不足,也不需要将程序交换到虚拟内存,相反可以将程序直接从内存中销毁。

       根据处理环境的不同,字符串常量也有可能被配置在可改写的内存区域,视需求而定。

   2. 指向函数的指针

       函数可以在表达式中被解读成“指向函数的指针“,“指向函数的指针”本质上也是指针(地址),所以可以将它赋给指针型变量。

       比如有下面的函数原型: int func(double d); 

       保存指向此函数的指针的变量的声明如下: int (*func_p)(double);

       然后写成下面这样,就可以通过 func_p 调用 func:

int (*func_p)(double);        // 声明func_p = func;               // 将func 赋给func_pfunc_p(0.5);                 // 此时,func_p 等同于func
       将“指向函数的指针”保存在变量中的技术经常被运用在如下场合: 

       GUI 中的按钮控件记忆“当自身被按下的时候需要调用的函数”;

       根据“指向函数的指针的数组”对处理进行分配 。

       后者的“指向函数的指针的数组”,像下面这样使用:

int (*func_table[])(double) = {    func0,   func1,    func2,     func3, };               ┊ func_table[i](0.5);   // 调用func_table[i]的函数,参数为0.5
        使用上面的写法,不用写很长的 switch case,只需通过 i 的值就可以对处理进行分配。



二、静态变量

   1. 什么是静态变量

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

   2. 分割编译和连接

       关于函数和全局变量,如果它们的名称相同,即使它们跨了多个源代码文件也被作为相同的对象来对待。进行这项工作的是一个被称为“链接器”的程序。 为了在链接器中将名称结合起来,各目标代码大多都具备一个符号表(symbol table)。请注意自动变量完全没有出现在符号表中。这是因为自动变量的地址是在运行时被决定的,它属于链接器管辖范围以外的对象。




三、自动变量(栈)

   1. 内存区域的重复使用

       在上一节中,我们看到 func1()的自动变量 func1_variable 和 func2()的自动变量 func2_variable 存在于完全相同的地址上。 在声明自动变量的函数执行结束后,自动变量就不能被使用了。因此,func1()执行结束后,func2()重复使用相同的内存区域是完全没有问题的。

       要点:

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

   2. 函数调用

       自动变量在内存中究竟是怎样被保存的?为了更加详细地了解这一点,还是让我们用下面这个测试程序做一下实验。

#include <stdio.h>void func(int a, int b){int c, d;printf("func:&a..%p &b..%p\n", &a, &b);printf("func:&c..%p &d..%p\n", &c, &d);}int main(void){int a, b;printf("main:&a..%p &b..%p\n", &a, &b);func(1, 2);return 0;}
       运行结果如下:

main:  &a..0xbfbfd9e4  &b..0xbfbfd9e0 func:  &a..0xbfbfd9d8  &b..0xbfbfd9dc func:  &c..0xbfbfd9cc  &d..0xbfbfd9c8
       如果把运行结果用图来说明,应该是下图所示。

       此外,在调用函数时,请留意为形参分配新的内存区域。我们经常听说“C 的参数都是传值,向函数内部传递的是实参的副本”其中的复制动作,其实就是在这里发生的。


       C 语言中,在现有被分配的内存区域之上以“堆积”的方式,为新的函数调用分配内存区域*。在函数返回的时候,会释放这部分内存区域供下一次函数调用使用。下图粗略地表现了这个过程。


       对于像这样使用“堆积”方式的数据结构,我们一般称为C 语言中,通常将自动变量保存在栈中。通过将自动变量分配在栈中,内存区域可以被重复利用,这样可以节约内存。一旦函数执行结束,自动变量的内存区域就会被释放!



四、利用 malloc()来进行动态内存分配(堆)

   1. malloc()的基础

       C 语言中可以使用 malloc()进行动态内存分配。malloc()根据参数指定的尺寸来分配内存块,它返回指向内存块初始位置的指针,经常被用于动态分配结构体的内存领域、分配执行前还不知道大小的数组的内存领域等。

<span style="font-family:Microsoft YaHei;font-size:18px;">p = malloc(size);</span>
       一旦内存分配失败(内存不足),malloc()将返回 NULL。利用 malloc()分配的内存被结束使用的时候,通过 free()来释放内存。

free(p);     // 释放 p 指向的内存区域
       以上是 malloc()的基本使用方式。 像这样能够动态地(运行时)进行内存分配,并且可以通过任意的顺序释放的记忆区域,称为堆(heap)。

       

       malloc()主要有以下的使用范例:
       动态分配结构体:
       假设有一个记录了图书信息的结构体BookData,

typedef struct BookData_tag {     char  title[64];  /*书名*/     int   price;     /*价格*/     char  isbn[32];   /* ISBN */} BookData;

       随着新图书的加入,显然图书的数目在动态地变化着,最终会产生大量的BookData。因此无法通过提前构造一个确定大小的数组来划分内存空间。通过下面的方式,就可以在运行时分配 BookData 的内存区域。

BookData *book_data_p;  /* 分配一个结构体BookData 的内存区域 */book_data_p = malloc(sizeof(BookData));
       分配可变长数组:

       在上述BookData中,书名title[64]也是固定大小的数组,这显然是不可取的。可以写成如下形式:

typedef struct BookData_tag {     char  *title;  /*书名*/     int   price;     /*价格*/     char  isbn[32];   /* ISBN */} BookData;BookData *book_data_p;     /* 这里 len 为标题字符数+1(空字符部分的长度) */ book_data_p -> title = malloc(sizeof(char) * len);

就能够只给标题字符串分配它必要的内存区域了。 此时,如果想要引用 title 中某个特定的字符,当然可以写成

book_data_p -> title[i];

       提醒一点,ANSI C 中,malloc()的返回值类型为 void*,void*类型的指针可以不强制转型地赋给所有的指针类型变量。因此,我们不要对 malloc()的返回值进行强制转型。因为 C 不是 C++。 另外,C++中可以将任意的指针赋给 void*类型的变量,但不可以将 void*类型的值赋给通常的指针变量。所以在 C++中,malloc()的返回值必须要进行强制转型。但是,如果是 C++,通常使用 new 来进行动态内存分配(也应该这样)。

       再提醒一点,malloc() 不是系统调用。C 的函数库中为我们准备了很多的函数(printf()等)。另外,标准库的一部分函数最终会调用“系统调用”。所谓系统调用,就是请求操作系统来帮我们做一些特殊的函数群。


       2. malloc()中、free()后发生了什么

       malloc()大体的实现是,从操作系统一次性地取得比较大的内存,然后将这些内存“零售”给应用程序。

       现实中的 malloc()的实现,在改善运行效率上下了很多工夫。这里我们只考虑最单纯的实现方式——通过链表实现。最朴素的实现就是如下图,在各个块之前加上一个管理区域,通过管理区域构建一个链表。


       malloc()遍历链表寻找空的块,如果发现尺寸大小能够满足使用的块,就分割出来将其变成使用中的块,并且向应用程序返回紧邻管理区域的后面区域的地址。free()将管理区域的标记改写成“空块”,顺便也将上下空的块合并成一个块。这样可以防止块的碎片化。如果不存在足够大的空块,就请求操作系统对空间进行扩容。

       现实中的处理环境,是不会这样单纯地实现 malloc()功能的。

       正如刚才所说,malloc()管理从操作系统一次性地被分配的内存,然后零售给应用程序,这是它大致的实现方式。 因此,一般来说调用 free()之后,对应的内存区域是不会立刻返还给操作系统的,那么这块内存区域其实是无法引用的,必须等到它被破坏后。


   3. 碎片化

       某些处理环境对 malloc()的实现会以随机的顺序分配、释放内存。 此时,内存被零零碎碎分割,会出现很多细碎的空块。并且,这些区域事实上是无法被利用的。 这种现象,我们称为碎片化(fragmentation)。

       将块向前方移动,缩小块之间的距离,倒是可以整合零碎的区域并将它们组合成较大的块。可是 C 语言将虚拟地址直接交给了应用程序,库的一方是不能随意移动内存区域的。

       在 C 中,只要使用 malloc()的内存管理过程,就无法根本回避碎片化问题。



0 0
原创粉丝点击