C Primer Plus 第12章(存储类别、链接和内存管理)

1. 存储类别

  • C 提供了多种不同的模型或 存储类别(storage class) 在内存中存储数据

  • 从硬件方面来看,被存储的每个值都占用一定的物理内存,C 语言把这样的一块内存称为对象(object)

    • 对象可以存储一个或多个值
    • 一个对象可能并未存储实际的值,但是它在存储适当的值时一定具有相应的大小
  • 从软件方面来看,程序需要一种方法访问对象

    int i = 1;
    
    • 该声明双剑了一个名为 i 的标识符(identifier)
      • 标识符是一个名称,在这种情况下,标识符可以用来 指定(designate) 特定对象的内容
      • 标识符遵循变量的命名规则
      • 标识符是软件(C 程序)指定硬件内存中给的对象的方式
      • 该声明还提供了存储在对象中的值
    int i = 1;
    int* a = &i;
    
    • a 是一个标识符,它指定了一个存储地址的对象
    • 表达式 *a 不是标识符,因为它不是一个名称
      • 它确实指定了一个对象
      • 它与 i 指定的对象相同
        • i 既是标识符也是左值
        • *a 既是表达式也是左值
  • 如果可以使用左值修改对象中的值,该左值就是一个可修改的左值(modifiable lvalue)

    char* c = "abc";
    
    • 可以设置 c 重新指向其他字符串,所以标识符 c 是一个可修改的左值
    • *c 指定了存储 ‘a’ 字符的数据对象,所以 *c 是一个左值,但不是一个可修改的左值
  • 字符串字面量本身指定了存储字符串的对象,所以它是一个左值,但不是一个可修改的左值

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

  • 标识符用于访问对象,可以用 作用域(scope)链接(linkage) 描述标识符,标识符的作用域和链接表明了程序的哪些部分可以使用它

  • 不同的存储类别具有不同的存储器、作用域和链接

1.1 作用域

  • 描述程序中可访问标识符的区域

  • 一个 C 变量的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域

  • 是用一对花括号括起来的代码区域

  • 定义在块中的变量具有块作用域(block scope),块作用域变量的可见范围是从定义处到包含该定义的块的末尾

  • 虽然函数的形式参数声明在函数的左花括号之前,但是它们也具有块作用域,属于函数体这个块

void abc() {
    printf("abc\n"); // 块作用域
}
  • 函数作用域(function scope) 仅用于 goto 语句的标签

  • 即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数

  • 函数原型作用域(function prototype scope) 用于函数原型中的形参名(变量名)

  • 范围从形参定义处到原型声明结束

  • 编译器在处理函数原型中的形参时只关心它的类型,而形参名通常无关紧要

void abc(int i) { // 函数原型作用域
}
  • 文件作用域(file scope)
  • 从它的定义处到该定义所在文件的末尾均可见
  • 文件作用域变量也称为全局变量(global variable)
#include <stdin.h>
int i = 1; // 文件作用域
int main() {
    
}

1.2 链接

  • C 变量有 3 中链接属性:外部链接、内部链接或无链接

  • 无链接变量:具有块作用域、函数作用域或函数原型作用域的变量

    • 这些变量属于定义它们的块、函数或原型私有
  • 外部链接或内部链接:具有文件作用域的变量

    • 外部链接变量可以在多文件程序中使用

      int i = 1;
      
    • 内部链接变量只能在一个翻译单元中使用

      static int i = 1;
      

1.3 存储期

  • 存储器描述了通过这些标识符访问的对象的生存期

  • C 对象有 4 中存储期:静态存储期、线程存储期、自动存储期、动态分配存储期

  • 静态存储期:对象在程序的执行期间一直存在

    • 文件作用域变量具有静态存储期
  • 线程存储期:程序执行可被分为多个线程,从被声明时到线程结束一直存在

  • 自动存储期:当程序进入定义这些变量的块时,为这些变量分配内存;当退出这个块时,释放刚才为变量分配的内存

    • 块作用域的变量通常都具有自动存储期
    • 把自动变量占用的内存视为一个可重复使用的工作区或暂存区
  • 5 种存储类别

存储类别存储期作用域链接声明方式
自动自动块内
寄存器自动块内,使用关键字 register
静态外部链接静态文件外部所有函数外
静态内部链接静态文件内部所有函数外,使用关键字 static
静态无链接静态块内,使用关键字 static

1.4 自动变量

  • 属于自动存储类别的变量具有自动存储期、块作用域且无链接

  • 声明在块或函数头种的任何变量都属于自动存储类别

  • 可以使用 auto 表示自动变量

    • 关键字 auto 是存储类别说明符(storage-class specifier)
  • 如果内层块种声明的变量与外层块种的变量同名

    • 内层块会隐藏外层块的定义
    • 但是离开内层块后,外层块变量的作用域又回到了原来的作用域

1.4.1 没有花括号的块

for (int i = 1; i < 3; i++)
    printf("i = %d\n", i);

1.4.2 自动变量的初始化

  • 自动变量不会初始化,除非显式初始化它
  • 可以用 非常量表达式(non-constant expression) 初始化自动变量,前提是所用的变量已在前面定义过
int a = 1;
int b = 2 * a;

1.5 寄存器变量

  • 寄存器与普通变量比,访问和处理这些变量的速度更快
  • 由于寄存器变量存储在寄存器而非内存中,所以无法获取寄存器变量的地址
  • 寄存器变量也是块作用域、无链接和自动存储期
  • 使用存储类别说明符 register 便可声明寄存器变量
register int i;
  • 编译器必须根据寄存器或最快可用内存的数量衡量你的请求
    • 或者直接忽略你的请求
      • 在这种情况下,寄存器就变成普通的自动变量
      • 但是仍然不能对该变量使用地址运算符

1.6 块作用域的静态变量

  • 静态变量(static variable) 在内存中不动

  • 这些变量和自动变量一样,具有相同的作用域,但是程序离开它们所在的函数后,这些变量不会消失

  • 这些变量具有块作用域、无链接和静态存储期

#include <stdio.h>

void abc();

void main() {
    for (int i = 0; i < 3; i++) {
        abc();
    }
    // a++ = 1, b++ = 1
    // a++ = 1, b++ = 2
    // a++ = 1, b++ = 3
}

void abc() {
    int a = 1;
    static int b = 1;
    printf("a++ = %d, b++ = %d\n", a++, b++);
}

1.7 外部链接的静态变量

  • 外部链接的静态变量具有文件作用域、外部链接和静态存储期

    • 该类别有时称为外部存储类别(external storage class)
    • 属于该类别的变量称为外部变量(external variable)
    • 把变量的 定义性声明(defining declaration) 放在所有函数的外面便创建了外部变量
  • 为了指出函数使用了外部变量,可以在函数中用关键字 extern 再次声明

  • 如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须用 extern 在该文件中声明该变量

int a;
extern int b; // b 定义在另一个文件,必须这样声明

int main() {
    extern int a; // 可选声明
}

1.7.1 初始化外部变量

  • 外部变量和自动变量相似,可以被显式初始化
  • 与自动变量不同的是,如果未初始化外部变量,它们会被自动初始化为 0
  • 只能使用常量表达式初始化文件作用域变量
int a = 1;
int b = 2 * a; // 错误,a 是变量

1.7.2 使用外部变量

#include <stdio.h>

int i = 0;
void abc();

int main() {
    while (i != -1) {
        abc();
    }
}

void abc() {
    printf("Enter : ");
    scanf("%d", &i);
}

1.7.3 定义和声明

int i = 1;

int main() {
    extern int i;
}
  • 第 1 次声明称为定义式声明(defining declaration)
    • 为变量预留了存储空间,该声明构成了变量的定义
  • 第 2 次声明称为引用式声明(referencing declaration)
    • 只告诉编译器使用之前已创建的 tern 变量,所以这不是定义
  • extern 表明该声明不是定义,因为它指示编译器去别处查询其定义

1.8 内部链接的静态变量

  • 内部链接的静态变量具有静态存储期、文件作用和内部链接
  • 在所有函数外部,用存储类别说明符 static 定义的变量具有这种存储类别
static int i = 1;
int main() {
    extern int i;
}
  • 过去称为外部静态变量(external static variable),现在为内部链接的静态变量(static variable with internal linkage)

1.9 多文件

  • C 通过在一个文件中进行定义式声明,然后在其他文件种进行引用式声明来实现共享
  • 除了定义式声明外,其他声明都要使用 extern 关键字

1.10 存储类别说明符

  • C 语言有 6 个关键字作为存储类别说明符
    • auto
    • register
    • static
    • extern
    • _Thread_local
    • typedef

1.11 存储类别和函数

  • C99 新增内联函数
  • 外部函数可以被其他文件的函数访问,但是静态函数只能用于其定义所在的文件
int abc() {
    
}

static int def() { // 其他文件不能调用
    
}

extern int ghi() {
    
}

2. 随机数函数和静态变量

  • rand() 函数生成随机数

3. 掷骰子

4. 分配内存:malloc() 和 free()

  • malloc() 函数,在程序运行时分配更多的内存
    • 接收一个参数:所需的内存字节数
    • malloc() 函数会找到合适的空间内存块,这样的内存是匿名的
    • 但是,它确实返回动态分配内存块的首字节地址
    • malloc() 函数的返回类型通常被定义为指向 char 的指针
    • malloc() 函数可用于返回指向数组的指针、指向结构的指针等,通常该函数的返回值会被强制转换为匹配的类型
    • 如果 malloc() 函数分配内存失败,将返回空指针
int* i;
i = (int*) malloc (30 * sizeof(int));
  • 创建数组的 3 种方法
    • 声明数组时,用常量表达式表示数组的维度,用数组名访问数组的元素
    • 声明变长数组时,用变量表达式表示数组的维度,用数组名访问数组的元素
    • 声明一个指针,调用 malloc() ,将其返回值赋给指针,使用指针访问数组的元素
  • 第 2 种和第 3 种方法可以创建动态数组(dynamic array),在程序运行时选择数组的大小和分配内存
int a[n];
int* i;
i = (int*) malloc (n * sizeof(int));
  • 通常,malloc() 要与 free() 配套使用
  • free() 函数的参数时之前 malloc() 返回的地址,该函数释放之前 malloc() 分配的内存
  • 动态分配内存的存储期从调用 malloc() 分配内存到调用 free() 释放内存位置
  • free() 的参数是一个指针
    • free() 所用的指针变量可以与 malloc() 的指针变量不同,但是两个指针必须存储相同的地址
int* i;
i = (int*) malloc (30 * sizeof(int));
free(i);

4.1 free() 的重要性

  • 静态内存的数量在编译时是固定的,在程序运行期间也不会改变
  • 自动变量使用的内存数量在程序执行期间自动增加或减少
  • 但是动态分配的内存数量只会增加,除非用 free() 进行释放
  • 如果耗尽所有的内存依然使用动态分配,引发的问题称为内存泄漏(memory leak)
    • 在内存使用结束后调用 free() 可避免这类问题发生

4.2 calloc() 函数

  • 与 malloc() 类似
  • 它把块中的所有位都设置为 0
  • free() 函数也可用于释放 calloc() 分配的内存
int* i;
i = (int*) calloc (30, sizeof(int));
free(i);

4.3 动态内存分配和变长数组

  • 变长数组(VLA)和调用 malloc() 在功能上的异同

    • 相同:两者都可用于创建在运行时确定大小的数组

    • 不同:

      • 变长数组是自动存储类型,程序离开变长数组定义所在的块时,变长数组占用的内存空间会被自动释放,不必使用 free()
      • malloc() 创建的数组不必局限在一个函数内访问

4.4 存储类别的动态内存分配

  • 静态存储类别:所用的内存数量在编译时确定,只要程序还在运行,就可以访问存储在该部分的数据

    • 在程序开始执行时被创建,在程序结束时被销毁
  • 自动存储类别:在程序离开块时消失

    • 随着程序调用函数和函数结束

    • 这部分的内存通常作为栈来处理

    • 新创建的变量按顺序加入内存,然后以相反的顺序销毁

  • 动态分配的内存在调用 malloc() 或相关函数时存在,在调用 free() 后释放

    • 这部分的内存由程序员管理
    • 内存块可以在一个函数中创建,在另一个函数中销毁
      • 未使用的内存块分散在已使用的内存块之间
      • 使用动态内存通常比使用栈内存慢

5. ANSI C 类型限定符

  • C90 新增两个属性:恒常性(constancy)易变性(volatility)

  • 这两个属性可以分别用关键字 const 和 volatile 来声明,以这两个关键字创建的类型是限定类型(qualified type)

  • C99 新增了第 3 个限定符: restrict ,用于提高编译器优化

  • C11 新增了第 4 个限定符: _Atomic

  • C11 提供一个可选库,由 stdatomic.h 管理,以支持并发程序设计,而且 _Atomic 是可选支持项

  • C99 为类型限定符增加了一个新属性:幂等(idempotent)

const const int i = 1; // 等效于 const int i = 1;

typedef const int a;
const a b = 8;

5.1 const 类型限定符

const int i;
i = 1; // 报错

const int i = 1;

5.1.1 在指针和形参声明中使用 const

const int* i;
// i 指向一个 int 类型的 const 值
// 可以设置该指针指向其他 const 值
int* const i;
// i 是一个 const 指针
// i 必须指向这个地址,但是地址的内容可以改变
const int* const i;
// i 既不能指向别处,它所指向的内容也不能改变
int const* i;
// 与 const int* i; 相同

5.1.2 对全局数据使用 const

  • 使用全局变量会暴露数据,程序的任何部分都能修改数据
  • const 可以避免这样的危险
  • 可以创建 const 变量、 const 数组和 const 结构
  • 在文件间共享 const 数据需要注意,有两种方法
    • 第 1 种方法:遵循外部变量的常用规则:在一个文件中使用定义式声明,在其他文件中使用引式声明(使用 extern 关键字)
    • 第 2 种方法:把 const 变量放在一个头文件种,然后在其他文件中包含该头文件

5.2 volatile 类型限定符

  • 告知计算机代理(不是变量所在的程序)可以改变该变量的值
  • 通常被用于硬件地址以及在其他程序或同时运行的线程种共享数据
volatile int i; // i 是一个易变的位置
volatile int* i; // i 是一个指向易变位置的指针
volatile int a;
volatile int b;
int i;

a = i;
{
    // 不使用 i 的代码块
}
b = i;
  • 智能的编译器会发现 i 使用了两次,而且没有数据更改
  • 于是编译器把 i 的值临时存储在寄存器中,当 b 要使用 i 时才从寄存器种获取,以节约时间
  • 这个过程称为告诉缓存(caching)

5.3 restrict 类型限定符

  • 允许编译器优化某部分代码以更好地支持计算
  • 它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式
int* restrict i = (int*) malloc (10 * sizeof(int));

5.4 _Atomic 类型限定符(C11)

  • C11 通过包含可选的头文件 stdatomic.h 和 threads.h ,提供了一些可选的管理方法
  • 要通过各种宏函数来访问原子类型
  • 当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象
int i;
i = 1;

_Atomic int i;
atomic_store(&i, 1);
// 在 i 种存储数据 1 的过程s,其他线程不能访问 i
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值