文章目录
- 1. 存储类别
- 1.1 作用域
- 1.1.1 块作用域
- 1.1.2 函数作用域
- 1.1.3 函数原型作用域
- 1.1.4 文件作用域
- 1.2 链接
- 1.3 存储期
- 1.3.1 静态存储期
- 1.3.2 线程存储期
- 1.3.3 自动存储期
- 1.4 自动变量
- 1.5 寄存器变量
- 1.6 块作用域的静态变量
- 1.7 外部链接的静态变量
- 1.7.1 初始化外部变量
- 1.7.2 使用外部变量
- 1.7.3 外部名称
- 1.7.4 定义和声明
- 1.8 内部链接的静态变量
- 1.9 多文件
- 1.10 存储类别说明符
- 1.11 存储类别和函数
- 2. 分配内存:malloc() 和 free()
- 2.1 free() 的重要性
- 2.2 calloc() 函数
- 2.3 动态内存分配和变长数组
- 2.4 存储类别和动态内存分配
- 3. ANSI 类型限定符
- 3.1 const 类型限定符
- 3.1.1 在指针和形参声明中使用 const
- 3.1.2 对全局数据使用 const
- 3.2 volatitle 类型限定符
- 3.3 restrict 类型限定符
- 3.4 _Atomic 类型限定符(C11)
1. 存储类别
程序员通过 C 的内存管理系统指定变量的作用域和生命期,实现对程序的控制。
从软件方面看,程序需要一种方法访问物理内存—— 声明变量。
int entity = 3; // 使用标识符 entity 指定内存区域
变量名不是指定硬件内存中对象的唯一途径。
int * pt = &entity; // pt 是一个标识符,指定内存区域的地址,*pt 不是标识符,与 entity 指定的对象相同。
int ranks[10];
1.1 作用域
作用域描述程序中可访问标识符的区域。一个 C 变量的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域。
1.1.1 块作用域
块是用一对花括号括起来的代码区域,块作用域变量的可见范围是从定义处到包含该定义的块的末尾。
double blocky(double cleo) // 块作用域
{
double patrick = 0.0; // 块作用域
...
return patrick;
}
double blocky(double cleo) // 块作用域
{
double patrick = 0.0; // 块作用域
int i;
for (i=0; i<10; i++)
{
double q = cleo * i;// q 的作用域开始,仅限于内层块
...
patrick *= q; // q 的作用域结束
}
}
以前,具有块作用域的变量都必须声明在块的开头。C99 标准放宽了这一限制,允许在块中的任意位置声明变量,即:
for (int i=0; i<10; i++)
...
C99 把块的概念扩展到 for 循环、while 循环、do while 循环和 if 语句所控制的代码。
1.1.2 函数作用域
仅用于 goto 语句的标签。即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数。
1.1.3 函数原型作用域
用于函数原型中的形参名:
int mighty(int mouse, double large);
函数原型作用域的范围是从形参定义处到原型声明结束,编译器在处理函数原型中的形参时只关心它的类型,而形参名无关紧要,即使有形参名,也不必与函数定义中的形参名相匹配。只有在变长数组中,形参名才有用。
1.1.4 文件作用域
变量的定义在函数的外面,具有文件作用域,具有文件作用域的变量,从它的定义处到该定义所在文件的末尾均可见。
int units=0;
void critic(void);
int main(void)
{
...
}
void critic(void)
{
...
}
变量 units 具有文件作用域,这样的变量可用于多个函数,文件作用域变量也称为全局变量。
1.2 链接
C 语言有 3 中链接属性:外部链接、内部链接和无链接。
- 具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量(除了文件作用域)。
- 具有文件作用域的变量可以是外部链接或内部链接。外部链接变量可以在多文件程序中使用,内部链接变量只能在一个翻译单元(将 .c 文件中的 .h 头文件中的内容替换后的文本)中使用。
内部链接的文件作用域描述仅限于一个翻译单元,用外部链接的文件作用域描述可延伸其它翻译单元的作用域。
内部链接的文件作用域==文件作用域
外部链接的文件作用域==程序作用域、全局作用域
int giants = 5; //外部链接的文件作用域
static int dodgers = 3; // 内部链接的文件作用域
int main()
{
...
}
...
该文件和同一程序的其它文件都可以使用变量 giants,dodgers 属于文件私有,该文件中的任意函数都可以使用它。
1.3 存储期
作用域和链接描述了标识符的可见性。存储期描述了通过这些标识符访问的对象的生存期。
4 种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。
1.3.1 静态存储期
如果对象具有静态存储期,那么它在程序的执行期间一直存在。
文件作用域具有静态存储期。对于文件作用域变量,关键字 static 表明了其链接属性,而非存储期。 以 static 声明的文件作用域具有内部链接,但是无论外部/内部链接,所有的文件作用域都具有静态存储周期。
1.3.2 线程存储期
用于并发程序设计,程序执行可被分为多个线程。具有线程存储期的对象,从被声明到线程结束一直存在。以关键字 _Thread_local 声明一个对象时,每个线程都获得该变量的私有备份。
1.3.3 自动存储期
块作用域的变量通常具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存;当退出这些块时;释放刚才为变量分配的内存。
变长数组稍有不同,它们的存储期从声明处到块的末尾,而不是从块的开始处到块的末尾。
// number 和 index 每次调用 bore() 函数时创建,在离开函数时被销毁;
void bore(int number)
{
int index;
for (idnex=0; index<number; index++)
...
}
块作用域变量也能具有静态存储期,加上 static 关键字即可。
void more(int number)
{
int index;
static int ct=0; // 存储在静态内存中,从程序被载入直到程序结束期间都存在,只有在执行该函数时,程序才能使用 ct 访问它所指定的对象(也可以提供指针形参或返回值来控制)
...
return 0;
}
作用域和链接描述了标识符的可见性。存储期描述了通过这些标识符访问的对象的生存期。
5 种存储类别
存储类别 | 存储期 | 作用域 | 链接 | 声明方式 |
自动 | 自动 | 块 | 无 | 块内 |
寄存器 | 自动 | 块 | 无 | 块内,使用关键字 register |
静态外部链接 | 静态 | 文件 | 外部 | 所有函数外 |
静态内部链接 | 静态 | 文件 | 内部 | 所有函数外,使用关键字 static |
静态无链接 | 静态 | 块 | 无 | 块内,使用关键字 static |
1.4 自动变量
具有自动存储期、块作用域且无链接
默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。
在块中,auto int plox;
和 int plox;
一样。
auto
是存储类别说明符,如果编写 C/C++ 兼容的程序,最好不要使用 auto 作为存储类别说明符。
块作用域和无链接意味着只有在变量定义所在的块中才能通过变量名访问该变量。另一个函数可以使用相同名变量,但是该变量是储存在不同内存位置上的另一个变量。
变量具有自动存储期意味着,程序在进入该变量声明所在的块时变量存在,程序在退出该块时变量消失,原来该变量占用的内存位置现在可做他用。
如果内层块中声明的变量与外层块中的变量同名会怎样?内层块会隐藏外层块的定义。
jiaming@jiaming-VirtualBox:~/Documents$ ./hiding.o
x in outer block: 30 at 0x7ffc3b5fd390
x in inner block: 77 at 0x7ffc3b5fd394
x in outer block: 30 at 0x7ffc3b5fd390
x in while loop: 101 at 0x7ffc3b5fd394
x in while loop: 101 at 0x7ffc3b5fd394
x in while loop: 101 at 0x7ffc3b5fd394
x in outer block: 34 at 0x7ffc3b5fd390
jiaming@jiaming-VirtualBox:~/Documents$ cat hiding.c
int main()
{
int x = 30;
printf("x in outer block: %d at %p\n", x, &x);
{
int x = 77;
printf("x in inner block: %d at %p\n", x, &x);
}
printf("x in outer block: %d at %p\n", x, &x);
while (x++ < 33)
{
int x = 100;
x++;
printf("x in while loop: %d at %p\n", x, &x);
}
printf("x in outer block: %d at %p\n", x, &x);
return 0;
}
1.5 寄存器变量
由于寄存器变量储存在寄存器而非内存中,所以无法获取寄存器变量的地址,寄存器变量是块作用域、无链接和自动存储期。使用存储类别说明符 register
可声明寄存器变量。
register int quick;
声明 变量为 register 类别与直接命令相比更像是一种请求。编译器必须根据寄存器或最快可用内存的数量衡量你的请求,或直接忽略你的请求,所以可能不会如你所愿。
1.6 块作用域的静态变量
具有块作用域、无链接,静态存储期。
静态变量并不是一个不可变的变量,静态的意思是该变量在内存中原地不动,并不是说它的值不变。具有文件作用域的变量自动具有静态周期。这些变量和自动变量一样,具有相同的作用域,但是程序离开它们所在的函数后,这些变量不会消失。
jiaming@jiaming-VirtualBox:~/Documents$ ./loc_stat.o
Here comes iteration 1:
fade = 1 and stay = 1
Here comes iteration 2:
fade = 1 and stay = 2
Here comes iteration 3:
fade = 1 and stay = 3
jiaming@jiaming-VirtualBox:~/Documents$ cat loc_stat.c
void trystat(void);
int main(void)
{
int count;
for (count=1; count<=3; count++)
{
printf("Here comes iteration %d:\n", count);
trystat();
}
return 0;
}
void trystat(void)
{
int fade = 1; // 每次调用都会初始化
static int stay = 1; // 只在编译 strstat() 时被初始化一次,静态变量和外部变量在程序被载入内存时已执行完毕。
printf("fade = %d and stay = %d\n", fade++, stay++);
}
不能在函数的形参中使用 static。
int wontwork(static int flu);// 不允许
1.7 外部链接的静态变量
具有文件作用域、外部链接和静态存储期
该类别有时称为外部存储类别,属于该类别的变量称为外部变量。可以在函数中使用关键字 extern
声明,也可以把变量的定义性声明放在所有函数的外面便创建了外部变量。
int Errupt; // 外部定义的变量
double Up[100]; // 外部定义的数组
extern char Coal; // 如果 Coal 被定义在另一个文件中,必须这样声明
void next(void);
int main()
{
extern int Errupt; // 可选的声明, int Errupt 将会隐藏外部定义的变量 Errupt,如果不得已要使用与外部同名的局部变量,可以在局部变量的声明中使用 auto 存储类别说明符明确表达这种意图
...
extern double Up[]; // 可选的声明,仅为了说明 main 函数要使用改变量
}
void next(void)
{
...
}
int Hocus;
int maigc();
int main(void)
{
extern int Hocus; // 之前已声明的外部变量
...
}
int magic()
{
extern int Hocus; // 与上面的 Hocus 是同一个变量
...
}
int Hocus; // 从声明处到文件结尾
int magic();
int main(void)
{
int Hocus; // 默认是自动变量 auto int Hocus 也可
...
}
int Pocus;
int magic()
{
auto int Hocus; // 把局部变量 Hocus 显式声明为自动变量
}
extern 关键字详解
一种变量声明形式,不能进行变量初始化(不能定义)。如果一个文件想使用另一个文件中的某个变量,还是使用 extern 关键字更好。
int main()
{
extern int num; // 不能在此处赋值,原位置赋值即可,之后可以改变num值,告诉编译器num这个变量是存在的,但是不是在这之前声明的,你到别的地方找找吧
printf("%d\n", num);
}
int num = 4;
// a.c
int main()
{
extern int num; // 不能在此处赋值,原位置赋值即可,之后可以改变num值,告诉编译器num这个变量是存在的,但是不是在这之前声明的,你到别的地方找找吧
printf("%d\n", num);
}
// b.c
int num = 4; // 全局变量
// a.c
int main()
{
extern void func(); // 还可以引用另一个文件中的函数
func();
}
// b.c
void func()
{
;
}
1.7.1 初始化外部变量
外部变量与自动变量不同的是,如果未初始化外部变量,它们会被自动初始化为 0。这一原则也适用于外部定义的数组元素。只能使用常量表达式初始化文件作用域变量。
1.7.2 使用外部变量
jiaming@jiaming-VirtualBox:~/Documents$ ./global.o
How many pounds to a firkin of butter?
14
No luck, my friend. Try again.
56
You must have looked it up!
jiaming@jiaming-VirtualBox:~/Documents$ cat global.c
int units = 0; // units 具有文件作用域、外部链接和静态存储期。
void critic(void);
int main()
{
extern int units;
printf("How many pounds to a firkin of butter?\n");
scanf("%d", &units);
while(units != 56)
critic();
printf("You must have looked it up!\n");
return 0;
}
void critic(void)
{
printf("No luck, my friend. Try again.\n");
scanf("%d", &units);
}
main() 函数和 critic() 都可以通过标识符 units 访问相同的变量。units 具有文件作用域、外部链接和静态存储期。
1.7.3 外部名称
C99 和 C11 标准都要求编译器识别局部标识符的前 63 个字符和外部标识符的前 31 个字符。
1.7.4 定义和声明
int tern = 1; // tern 被定义,为变量预留了存储空间,该声明构成了变量的定义,定义式声明
main()
{
extern int tern; // 使用在别处定义的 tern,只是告诉编译器使用之前已创建的 tern 变量,而不是定义 引用式声明,关键字 extern 表明该声明不是定义,因为它只是编译器去别处查询其定义
extern int tern;
int main(void)
{
// 编译器会假设 tern 实际的定义在该程序的别处,也许在别的文件中。该声明并不会引起分配存储空间。不要用 extern 来创建外部定义,只用它来引用现有的外部定义
// 外部变量只能初始化一次,且必须在定义该变量时进行。
// file_one.c
char permis = 'N';
// file_two.c
extern char permis = 'Y'; // 错误
// file_two 中的声明是错误的,因为 file_one.c 中的定义式声明已经创建并初始化了 permis
1.8 内部链接的静态变量
静态存储期、文件作用域、内部链接
static int svil = 1; // 静态变量,内部链接
int main(){
普通的外部变量可用于同一程序中任意文件中的函数(外部定义 int a;
),但是内部链接变量只能用于同一个文件中的函数外部定义 static int a;
。可以使用存储类别说明符 extern,在函数中重复声明任何具有文件作用域的变量。
int traveler = 1;// 外部链接
static int stayhome = 1; // 内部链接
int main()
{
extern int traveler; // 使用定义在别处的 traveler
...
}
对于该程序的翻译单元,traveler 和 stayhome 都具有文件作用域,但只有 traveler 可用于其它翻译单元(因为它具有外部链接)。
1.9 多文件
只有当程序由多个翻译单元组成时,才体现区别内部链接和外部链接的重要性。
复杂的 C 程序通常由多个单独的源代码文件组成。有时,这些文件可能要共享一个外部变量。C 通过在一个文件中进行定义式声明,然后在其他文件中进行引用式声明来实现共享。也就是说,除了一个定义式声明外,其他声明都要使用 extern 关键字。而且,只有定义式声明才能初始化变量。
如果外部变量定义在一个文件中,那么其他文件在使用该变量之前必须先声明它(用 extern 关键字)。在某文件中对外部变量进行定义式声明只是单方面允许其他文件使用该变量,其他文件在用 extern 声明之前不能直接使用它。
1.10 存储类别说明符
关键字 static
和 extern
的含义取决于上下文。C 语言有 6 个关键字作为存储类别说明符:auto
、register
、static
、extern
、_Thread_local
、typedef
。
- typedef:作为关键字与任何内存存储无关,把它归于此类有一些语法上的原因,在绝大数情况下,不能在声明中使用多个存储类别说明符,意味着不能使用多个存储类别说明符作为 typedef 的一部分。
- auto:表明变量是自动存储期,只能用于块作用域的变量声明中。由于在块中声明的变量本身就具有自动存储期,所以使用 auto 主要是为了明确表达要使用与外部变量同名的局部变量的意图。
- register:只用于块作用域的变量,它把变量归为寄存器存储类别,请求最快速度访问改变量。同时,还保护了该变量的地址不被获取。
- static:创建的对象具有静态存储期,载入程序时创建对象,当程序结束时对象消失。如果 static 用于文件作用域声明,作用域受限于该文件。如果 static 用于块作用域声明,作用域则受限于该块。因此,只要程序在运行对象就存在并保留其值,但是只有在执行块内的代码时,才能通过标识符访问。块作用域的静态变量无链接。文件作用域的静态变量具有内部链接。
- extern:表明声明的变量定义在别处。如果包含 extern 的声明具有文件作用域,则引用的变量必须具有外部链接。如果包含 extern 的声明具有块作用域,则引用的变量可能具有外部链接或内部链接。
自动变量具有块作用域、无链接、自动存储期。它们是局部变量,属于其定义所在块私有。寄存器变量的属性和自动变量相同,但是编译器会使用更快的内存或寄存器储存它们。不能获取寄存器变量的地址。
具有静态存储期的变量可以具有外部链接、内部链接或无链接。在同一个文件所有函数的外部声明的变量是外部变量,具有文件作用域、外部链接和静态存储期。如果在这种声明前面加上关键字 static,那么其声明的变量具有文件作用域、内部链接和静态存储期。如果在函数中用 static 声明一个变量,则该变量具有块作用域、无链接、静态存储期。
具有自动存储期的变量,程序在进入该变量的声明所在块时才为其分配内存,在退出该块时释放之前分配的内存。如果未初始化,自动变量中是垃圾值。程序在编译时为具有静态存储器的变量分配内存,并在程序的运行过程中一直保留这块内存,如果未初始化,这样的变量会被设置为 0。
具有块作用域的变量是局部的,属于包含该声明的块私有。具有文件作用域的变量对文件中位于其声明后面的所有函数可见。具有外部链接的文件作用域变量,可用于该程序的其他翻译单元,具有内部链接的文件作用域变量,只能用于其声明所在的文件内。
jiaming@jiaming-VirtualBox:~/Documents$ cc parta.c partb.c -o out.o
jiaming@jiaming-VirtualBox:~/Documents$ ./out.o
Enter a positive integer (0 to quit): 5
loop cycle: 1
subtotal: 15; total: 15
Enter a positive integer (0 to quitt): 10
loop cycle: 2
subtotal: 55; total: 70
Enter a positive integer (0 to quitt): 2
loop cycle: 3
subtotal: 3; total: 73
Enter a positive integer (0 to quitt): 0
Loop executed 3 times
jiaming@jiaming-VirtualBox:~/Documents$ cat parta.c
void report_count();
void accumulate(int k);
int count = 0; // 文件作用域,外部链接
int main(void)
{
int value; // 自动变量
register int i;// 寄存器变量
printf("Enter a positive integer (0 to quit): ");
while (scanf("%d", &value) == 1 && value > 0)
{
++count; // 使用文件作用域变量
for (i = value; i >= 0; i--)
accumulate(i);
printf("Enter a positive integer (0 to quitt): ");
}
report_count();
return 0;
}
void report_count()
{
printf("Loop executed %d times\n", count);
}
jiaming@jiaming-VirtualBox:~/Documents$ cat partb.c
extern int count;//引用式声明,外部链接
static int total = 0;//静态定义,内部链接
void accumulate(int k);//函数原型
void accumulate(int k)//k具有块作用域,无链接
{
static int subtotal = 0;//静态,无链接
if (k <= 0)
{
printf("loop cycle: %d\n", count);
printf("subtotal: %d; total: %d\n", subtotal, total);
subtotal = 0;
}
else
{
subtotal += k;
total += k;
}
}
1.11 存储类别和函数
函数也有存储类别,可以是外部函数或静态函数。C99 新增了第 3 种类别 —— 内联函数。外部函数可以被其他文件的函数访问,但是静态函数只能用于其定义所在的文件。假设一个文件中包含了以下函数原型:
double gamma(double);
static double beta(int, int);
extern double delta(double, int);
在同一个程序中,其他文件中的函数可以调用 gamma() 和 delta(),但是不能调用 beta(),因为以 static 存储类别说明符创建的函数属于特定模块私有,避免了名称冲突的问题。
保护性程序设计的黄金法则是:“按需知道”原则,尽量在函数内部解决该函数的任务,只共享那些需要共享的变量。
extern int x;
int main()
{
printf("%d\n", x);
}
int x = -1;
2. 分配内存:malloc() 和 free()
之前是确定用哪种存储类别后,根据已制定好的内存管理规则,将自动选择其作用域和存储期。
静态数据在程序载入内存时分配,而自动数据在程序执行块时分配,并在程序离开时该块销毁。
C 可以在运行时分配更多的内存。主要的工具是 malloc()
函数,该函数接受一个参数:所需的内存字节数。malloc() 函数会找到合适的空闲内存块,这样的内存是匿名的,malloc() 分配内存,但是不会为其赋名。 它确实会返回动态分配内存块的首字节地址。因此,可以把该地址赋给一个指针变量,并使用指针访问这块内存。malloc() 的返回类型通常被定义为指向 char 的指针。从 ANSI C 标准开始,C 使用一个新的类型:指向 void 的指针。该类型相当于一个“通用指针”。malloc() 函数可用于返回指向数组的指针、指向结构的指针等。在 ANSI C 中,应该坚持使用强制类型转换,提高代码的可读性。然而,把指向 void 的指针赋给任意类型的指针完全不用考虑类型匹配的问题。如果 malloc() 分配内存失败,将返回空指针。
用 malloc() 创建一个数组,除了用 malloc() 在程序运行时请求一块内存,还需要一个指针记录这块内存的位置。
double *ptd;
ptd = (double*)malloc(30 * sizeof(double));
以上代码为 30 个 double 类型的值请求内存空间,并设置 ptd 指向该位置。如果让 ptd 指向这个块的首元素,便可像使用数组名一样使用它。也就是说,可以使用表达式 ptd[0] 访问该块的首元素,ptd[1]访问第 2 个元素。
我们有 3 种创建数组的方法:
- 声明数组时,用常量表达式表示数组的维度,用数组名访问数组的元素。可以用静态或自动内存创建这种数组。
- 声明变长数组(C99新增的特性)时,用变量表达式表示数组的维度,用数组名访问数组的元素。具有这种特性的数组只能在自动内存中创建。
- 声明一个指针,调用 malloc(),将其返回值赋给指针,使用指针访问数组的元素。该指针可以是静态的或自动的。
在 C99 之前,不能这样做:
double item[n];
但是,可以这样做:
ptd = (double *)malloc(n*sizeof(double));
通常,malloc() 要与 free() 配套使用。free() 函数的参数是之前 malloc() 返回的地址,该函数释放之前 malloc() 分配的内存。因此,动态分配内存的存储期从调用 malloc() 到 free() 释放内存为止。二者的原型都定义在 stdlib.h。
使用 malloc(),程序可以在运行时才确定数组大小。
jiaming@jiaming-VirtualBox:~/Documents$ ./dyn_arr.o
What is the maximum number of type double entries?
5
Enter the values (q to quit):
20 30 35 25 40 80
Here are your 5 entried:
20.00 30.00 35.00 25.00 40.00
Done.
jiaming@jiaming-VirtualBox:~/Documents$ cat dyn_arr.c
int main()
{
double *ptd;
int max;
int number;
int i = 0;
puts("What is the maximum number of type double entries?");
if (scanf("%d", &max) != 1)
{
puts("Number not correctly entered -- bye.");
exit(EXIT_FAILURE);
}
ptd = (double *)malloc(max * sizeof(double)); // 强制类型转换在 C++ 中必须使用
if (ptd == NULL)
{
puts("Memory allocation failed. Goodbye.");
exit(EXIT_FAILURE);
}
puts("Enter the values (q to quit):");
while (i < max && scanf("%lf", &ptd[i]) == 1)
++i;
printf("Here are your %d entried:\n", number = i);
for (i = 0; i < number; i++)
{
printf("%7.2f", ptd[i]);
if (i % 7 == 6)
putchar('\n');
}
if (i % 7 != 0)
putchar('\n');
puts("Done.");
free(ptd);
return 0;
}
2.1 free() 的重要性
- 静态内存的数量在编译时是固定的,在程序运行期间也不会改变。
- 自动变量使用的内存数量在程序执行期间自动增加或减少。
- 动态分配的内存数量只会增加,除非用 free() 进行释放。
耗尽所有内存称之为内存泄漏,在函数末尾调用 free() 函数可避免这类问题发生。
2.2 calloc() 函数
另一个内存分配函数。
long * newmem;
newmem = (long*)calloc(100, sizeof(long));
和 malloc() 类似,在 ANSI 之前,calloc() 也返回指向 char 的指针,在 ANSI 之后,返回指向 void 的指针。如果要存储不同的类型,应该使用强制类型转换运算符。
接受两个无符号整数作为参数,第 1 个参数是所需的存储单元数量,第 2 个参数是存储单元的大小(bytes)。 它也会将块中所有位都设置为 0。
2.3 动态内存分配和变长数组
变长数组是自动存储类型,程序离开变长数组定义所在块的时候,变长数组占用的内存空间会被自动释放,不必使用 free()。
malloc() 创建的数组不必局限在一个函数内访问。free() 所用的指针变量可以与 malloc() 的指针变量不同,但是两个指针必须存储相同的地址,但是,不能释放同一块内存两次。
对多维数组而言,使用变长数组更方便。
int n = 5;
int m = 6;
int ar2[n][m]; // nxm的变长数组(VLA)
int (*p2)[6]; // C99 之前的写法, 表明 p2 指向一个内含 6 个 int 类型的数组。p2[i] 代表一个由 6 个整数构成的元素,p2[i][j] 代表一个整数
int (*p3)[m]; // 要求支持变长数组
p2 = (int (*)[6]) malloc(n * 6 * sizeof(int)); // nx6 数组
p3 = (int (*)[m]) malloc(n * m * sizeof(int));
2.4 存储类别和动态内存分配
内存可分为 3 个部分:
- 一部分供具有外部链接、内部链接和无链接的静态变量使用;
- 一部分供自动变量使用;通常用栈来处理,这意味着新创建的变量按顺序加入内存,然后以相反的顺序销毁。
- 一部分供动态内存分配;通常使用堆来处理,程序员管理,内存可能会支离破碎,动态内存通常比使用栈内存慢。
3. ANSI 类型限定符
除了常用类型和存储类别来描述一个变量,C90 新增了两个属性:恒常性(constancy)const
和易变性(volatility)volatitle
。以这两个关键字创建的类型是限定类型。C99 还新增了第 3 个限定符:restrict
,用于提高编译器优化。C11标准新增了第 4 个限定符:_Atomic
。
C99 为类型限定符新增了一个属性:幂等性。即:
const const const int n = 6; // 等价于 const int n = 6;
typedef const int zip;
const zip q = 8;
3.1 const 类型限定符
const int nochange; // 限定 nochange 的值不能被修改
nochange = 23; // 不允许
const int nochange = 12; // 没问题,让 nochange 称为只读变量,初始化后不能改变它的值
3.1.1 在指针和形参声明中使用 const
指针需要区分是限定指针本身为 const 还是限定指针指向的值为 const。
const float *pf;// pf 指向一个 float 类型的 const 值,pf 指向的值不能改变,而 pf 指向的地址可以改变
float * const pt; // 一个 const 指针,pt 本身的值不可改变,pt 必须指向同一个地址,但是它所指向的值可以改变
const float * const ptr; // ptr 既不能指向别处,所指向的值也不能改变
float cont * pfc; // const float * pfc 相同
const 放在 * 左侧任意位置,限定了指针指向的数据不能改变;const 放在 * 的右侧任意位置,限定了指针本身不能改变(地址不变)。
3.1.2 对全局数据使用 const
使用 const 限定符声明全局数据结构很合理。
// file1.c
const double PI = 3.1415926;
const chat * MONTHS[12] = {
"January", "February", "March", "April", "May",
"June", "July", "August", "September", "October",
"November", "December"
};
// file2.c
extern const double PI;
extern const * MONTHS [];
// 必须在头文件中使用 static 声明全局 const 变量,如果去掉,将导致每个包含该头文件的文件中都有一个相同标识符的定义式声明
// constant.h
const double PI = 3.1415926;
const chat * MONTHS[12] = {
"January", "February", "March", "April", "May",
"June", "July", "August", "September", "October",
"November", "December"
};
// file.c
3.2 volatitle 类型限定符
volatitle 限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值。 通常,它被用于硬件地址以及其他程序或同时运行的线程中共享数据。例如,一个地址上可能储存着当前的时钟时间,无论程序做什么,地址上的值都随时间的变化而改变。
该限定符设计编译器的优化。
val1 = x;
// 一些不使用 x 的代码
val2 = x;
智能的编译器会注意到以上代码使用了两次 x,但并未改变它的值。于是编译器把 x 的值临时储存在寄存器中,然后再 val2 需要使用 x 时,才从寄存器中(而不是原始内存位置上)读取 x 的值,以节约时间。这个过程被称为高速缓冲(caching)。但是如果一些其他代理在以上两条语句之间改变了 x 的值,就不能这样优化,如果没有 volatitle 关键字,编译器就不知道这种事情是否发生。
可以同时使用 const 和 volatitle 限定一个值,通常用 const 把硬件时钟设置为程序不能更改的变量,但是可以通过代理改变,这时用 volatitle,二者顺序不重要。
3.3 restrict 类型限定符
restrict 关键字运行编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。
举例来说,
int ar[10];
int * restrict restar = (int *) malloc(10 * sizeof(int));
int * par;
指针 restar 是访问 malloc() 所分配内存的唯一且初始的方式。因此,可以用 restrict 关键字限定它。而指针 par 既不是访问 ar 数组中数据的初始方式,也不是唯一方式,所以不用把它设置为 restrict。
for (n = 0; n < 10; n++)
{
par[n] += 5;
restar[n] += 5;
ar[n] *= 2;
par[n] += 3;
restar[n] += 3;
}
由于之前声明了 restar 是访问它所指向的数据块的唯一且初始的方式,编译器可以把涉及 restar 的两条语句替换成下面这条语句,效果相同:
restar[n] += 8;
但是,如果把与 par 相关的两条语句替换成下面的语句,将导致计算错误:
par[n] += 8;
因为 for 循环在 par 两次访问相同的数据之前,用 ar 改变了该数据的值。
如果使用了 restrict 关键字,编译器就可以选择捷径优化计算。
restrict 关键字有两个读者,一个是编译器,该关键字告知编译器可以自由假定一些优化方案。另一个读者是用户,该关键字告知用户要使用满足 restrict 要求的参数。
3.4 _Atomic 类型限定符(C11)
并发程序设计把程序执行分成可以同时执行的多个线程。这个程序设计带来了新的挑战,包括如何管理访问相同数据的不同线程。C11 通过可选的头文件 stdatomic.h 和 threads.h,提供了一些可选的管理办法。值得注意的是,要通过各种宏函数来访问原子类型,当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象。
_Atomic int hogs; // 原子类型的变量
atomic_store(&hogs, 12);// stdatomic.h 中的宏
// 在 hogs 中存储 12 是一个原子过程,其他线程不能访问 hogs.