导航

C 存储类别、链接和内存管理

发布时间:5 个月前 更新时间:4 months ago
开发

存储类别

C 提供了多种不同的模型或存储类别(storage class) 在内存中存储数据。先要复习一些概念和术语。

从硬件方面看,被存储的每一个值都占用一定的物理内存,C 语言把这样的一块内存称为对象(Object),或者准确点说称为内存对象(Memory Object)对象 可以存储一个或多个值。一个对象刚初始化时,没有存储实际的值(NULL),但是当它存储适当的值时一定具有相应的大小。

从软件方面看,程序需要一种方法访问对象。可以通过声明变量来完成:

int entity = 3;

该声明创建了一个名为 entity标识符(identifier)。标识符是一个名称,在软件层面上代表物理层面上内存对象这个概念的名称。在通过这个方式声明时,标识符entity可以用来**指定(designate)**特定对象的内容。在这里标识符所表示的内存对象的值是3

变量名不是指定对象的唯一途径,我们还可以通过表达式来进行运算,用以获取内存对象:

int *pt = &entity;
int ranks[10];

pt是一个标识符,是一个存储地址的对象。 但是,表达式 *pt 不是标识符,因为它不是一个名称。然而,它确实指定了一个对象,在这种情况下,它与 entity 指定的对象相同。

一般而言,那些指定对象的表达式被称为 左值。所以,entity 即是标识符也是左值。

*pt 即是表达式也是左值。按照这个思路,ranks + 2 * entity 既不是标识符,也不是左值,它就是一个指针运算表达式。但是*(ranks + 2 * entity),是一个左值,因为ranks + 2 * entity 表达式计算出的内存地址被经过*取值运算符,取出了其中的值,它就是一个对象。我们可以使用数学中的换元概念来表示:

int ranks[10];
设:
    int *v;
    v = (ranks + 2 * entity);
那么:
    *(v) = *(ranks + 2 * entity)
当 ranks + 2 * entity 的位置为数字 10 时,

*v = 10;

在上文中,你应该理解了左值的概念,我们来重新整理一下,entity 我们称为标识符,方便我们去记忆某一个对象。 而左值不一定是标识符,也可以是一个运算表达式,但是左值必须能够表示一个内存对象。

现在我们引入新的概念,**可修改的左值(modifiable value)**和不可修改的左值。,用以引出const限定符的概念。现在我们有如下声明:

const char *pc = "Behold a string literal!";

程序根据该声明把相应的字符串字面量存储在内存中,那么包含这些字符值的数组就是一个对象,我们习惯称为数组对象(Array Object)。由于数组中的每一个字符都能够被单独访问,所以每一个字符也是一个对象。该声明还创建了。而一个标识符为 pc 的对象,存储着字符串的地址。由于可以设置 pc 重新只想其他字符串,所以标识符pc是一个可修改的左值。

const 只能保证被pc指向的字符串内容不被改变,但是无法保证 pc 不指向别的字符串。现在我们用上面的知识来描述,pc 是一个对象,该对象指向了另外一个对象,这个对象是字符串字面量在内存中的地址,而const 保证的是一个对象的值不能够被修改,这也就意味着,如果pc 是一个存储字面量3的变量时,字面量3是不可被修改的。

我们接着回到刚才的话题,由于*pc 指定了存储 B 字符的数据对象,所以 *pc 是一个左值,但不是一个可修改的左值(因为 const 限定符的存在)。与此类似,因为字符串字面量本身制定了存储字符串的对象,所以它也是一个左值,但不是可修改的左值。

可以用存储期(storage duration) 描述对象,所谓存储期是指对象在内存中保留了多长时间。

标识符用于访问对象,可以用 作用域(scope)链接(linkage) 描述标识符,标识符的作用域和链接表明了程序哪些部分可以使用它。不同的存储类别具有不用的存储期、作用域和链接。标识符可以在源代码的多文件中共享、可用于特定文件的任意函数中、可仅限于特定函数中使用,甚至只在函数中的某部分使用。

对象可存在于程序的执行期,也可以仅存在于它所在函数的执行期。对于并发编程,对象可以在特定现场的执行期存在。可以通过函数调用的方式显式分配和释放内存。

作用域

作用域在C语言中分为四个范围:

  1. 块作用域
  2. 函数作用域:
  3. 函数原型作用域
  4. 文件作用域

块作用域(block scope)

{}包含的代码块,在代码块中声明使用的标识符不能够在块范围外访问(编译器级别限制)。虽然函数的形参声明在{左边的小括号中,但是形参也具有块作用域,属于函数块这个块作用域中。

变量a 和 变量b的作用域都仅限于 foo 函数这个块作用域中:

int foo(int a, int b) {
    ...
    return 0;
}

但是在下面的for循环中,我们声明了4个变量,它们分别是:

  1. a:函数作用域
  2. b:函数作用域
  3. ifor 循环作用域
  4. loop_varfor 循环作用域
int blocky(int a) {
    int b = 0; 

    for (i = 0; i < 10; i++>) {
        int loop_var = a * i; // loop_var 作用域开始
        b = a;
    } // loop_var 作用域结束

    // 当我在这里试图访问 printf("%d", loop_var); 会出现错误
    ...
    return b;
}

当在循环体外部访问 loop_vari 变量时会出现错误。不过相较于 i 变量而言,loop_var 的生命周期更短,因为它会在每次循环时去重新初始化一个新的 loop_var ,而i是在整个循环周期中一直存在。

函数作用域(function scope)

函数作用域这个概念只作用于foto 语句标签。这意味着即使一个标签首次出现在函数的内城块中,它的作用域也延伸至整个函数。如果在两个块中使用相同的标签会很混乱,标签的函数作用域防止了这样的事情发生。在函数外,是不能够定义 goto 标签的,所以别问。

函数原型作用域(function prototype scope)