2.3 函数和字符串常量

2.3.1 只读内存区域

在我的处理环境中,函数(程序自身)和字符串常量被配置在内存里相邻的地址上。

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

由于函数本身不可能需要改写,所以它被配置在内存的只读区域。其实在很久以前,机器语言的程序改写自身程序代码的技术是被大量使用的*,但是现在的操作系统几乎都禁用了这种技术。

* 笔者就曾经有这样的经历。在 Z80 的情况下,只能通过直接指定地址的方式调用子程序……当然这些都是老皇历了。

那些能够修改自身代码的程序代码是非常晦涩难懂的。此外,如果执行程序是只读的,在同一份程序被同时启动多次的时候,通过在物理地址上共享程序能够节约物理内存。此外,由于硬盘上已经存放了可执行程序,就算内存不足,也不需要将程序交换到虚拟内存,相反可以将程序直接从内存中销毁*

* 大体上, Windows 、UNIX 等操作系统就是这样实现的。因此,在一部分 UNIX 中,如果在程序运行的时候将该程序改写(再编译/连接等),当前运行的程序就会崩溃。在如今的处理环境中,经常会对程序加锁。

根据处理环境的不同,字符串常量也有可能被配置在可改写的内存区域。像 DOS 这样不实施内存保护的操作系统也就罢了,可是在 UNIX 中,也有将字符串常量配置在非只读内存区域的情况。

假设有下面这样一个函数:

  1. void func(void)
  2. {
  3. char *str = "abc";
  4. printf("str..%s\n", str);
  5. :
  6. : 省略的很多逻辑
  7. str[0] = 'd';
  8. }

一旦允许改写字符串常量,第一次调用函数输出“abc”,第二次调用函数却会输出“dbc”。可是根据代码的逻辑,给 str 赋值“abc”后,紧接着就会输出 str 的值。尽管如此,第二次调用还是输出了“dbc”,这就很让人头大。

2.3.2 指向函数的指针

函数可以在表达式中被解读成“指向函数的指针”,因此,正如代码清单 2-2 的实验那样,写成 func 就可以取得指向函数的指针。

“指向函数的指针”本质上也是指针(地址),所以可以将它赋给指针型变量。

比如有下面的函数原型:

  1. int func(double d);

保存指向此函数的指针的变量的声明如下:

  1. int (*func_p)(double);

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

  1. int (*func_p)(double); ←声明
  2. func_p = func; ←将func 赋给func_p
  3. func_p(0.5); ←此时,func_p 等同于func

将“指向函数的指针”保存在变量中的技术经常被运用在如下场合:

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

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

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

  1. int (*func_table[])(double) = {
  2. func0,
  3. func1,
  4. func2,
  5. func3,
  6. };
  7. func_table[i](0.5); ←调用func_table[i]的函数,参数为0.5

使用上面的写法,不用写很长的 switch case,只需通过 i 的值就可以对处理进行分配。

哦?不明白为什么?

确实,像

  1. int (*func_p)(double); ←指向函数的指针

还有,

  1. int (*func_table[])(double); ←指向函数的指针的数组

这样的声明,是不能用普通的方法来读的。

关于这种声明的解读方式,会在第 3 章进行说明。