1. 存储类别
-
C 提供了多种不同的模型或 存储类别(storage class) 在内存中存储数据
-
从硬件方面来看,被存储的每个值都占用一定的物理内存,C 语言把这样的一块内存称为对象(object)
- 对象可以存储一个或多个值
- 一个对象可能并未存储实际的值,但是它在存储适当的值时一定具有相应的大小
-
从软件方面来看,程序需要一种方法访问对象
int i = 1;
- 该声明双剑了一个名为 i 的标识符(identifier)
- 标识符是一个名称,在这种情况下,标识符可以用来 指定(designate) 特定对象的内容
- 标识符遵循变量的命名规则
- 标识符是软件(C 程序)指定硬件内存中给的对象的方式
- 该声明还提供了存储在对象中的值
int i = 1; int* a = &i;
- a 是一个标识符,它指定了一个存储地址的对象
- 表达式 *a 不是标识符,因为它不是一个名称
- 它确实指定了一个对象
- 它与 i 指定的对象相同
- i 既是标识符也是左值
- *a 既是表达式也是左值
- 该声明双剑了一个名为 i 的标识符(identifier)
-
如果可以使用左值修改对象中的值,该左值就是一个可修改的左值(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