【《C Primer Plus》读书笔记】第12章:存储类别、链接和内存管理
12.1 存储类别
一些概念和术语:
- 对象:存储数据的一块内存
- 标识符:一个名称,软件指定硬件内存中的对象的方式
- 左值:指定对象(即指定内存位置上的内容)的表达式
12.1.1 作用域
作用域是描述程序中可访问标识符的区域。
一个C变量的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域。
块是用一对花括号括起来的代码区域,定义在块中的变量具有块作用域,变量的可见范围是从定义处到包含该定义的块的末尾。
注:虽然函数的形参定义在函数的花括号外,但是它们属于函数体这个块。
12.1.2 作用域规则
任何一种编程中,作用域是程序中定义的变量所存在的区域,超过该区域变量就不能被访问。
C 语言中有三个地方可以声明变量:
- 在函数或块内部的局部变量
- 在所有函数外部的全局变量
- 在形式参数的函数参数定义中
让我们来看看什么是局部变量、全局变量和形式参数。
局部变量
在某个函数或块的内部声明的变量称为局部变量。它们只能被该函数或该代码块内部的语句使用。局部变量在函数外部是不可知的。下面是使用局部变量的实例。在这里,所有的变量 a、b 和 c 是 main() 函数的局部变量。
#include <stdio.h>
int main ()
{
/* 局部变量声明 */
int a, b;
int c;
/* 实际初始化 */
a = 10;
b = 20;
c = a + b;
printf ("value of a = %d, b = %d and c = %d\n", a, b, c);
return 0;
}
全局变量
全局变量是定义在函数外部,通常是在程序的顶部。全局变量在整个程序生命周期内都是有效的,在任意的函数内部能访问全局变量。
全局变量可以被任何函数访问。也就是说,全局变量在声明后整个程序中都是可用的。下面是使用全局变量和局部变量的实例:
#include <stdio.h>
/* 全局变量声明 */
int g;
int main ()
{
/* 局部变量声明 */
int a, b;
/* 实际初始化 */
a = 10;
b = 20;
g = a + b;
printf ("value of a = %d, b = %d and g = %d\n", a, b, g);
return 0;
}
注:在程序中,局部变量和全局变量的名称可以相同,但是在函数内,如果两个名字相同,会使用局部变量值,全局变量不会被使用。
形式参数
函数的参数,形式参数,被当作该函数内的局部变量,如果与全局变量同名它们会优先使用。下面是一个实例:
#include <stdio.h>
/* 全局变量声明 */
int a = 20;
int main ()
{
/* 在主函数中的局部变量声明 */
int a = 10;
int b = 20;
int c = 0;
int sum(int, int);
printf ("value of a in main() = %d\n", a);
c = sum( a, b);
printf ("value of c in main() = %d\n", c);
return 0;
}
/* 添加两个整数的函数 */
int sum(int a, int b)
{
printf ("value of a in sum() = %d\n", a);
printf ("value of b in sum() = %d\n", b);
return a + b;
}
当上面的代码被编译和执行时,它会产生下列结果:
value of a in main() = 10
value of a in sum() = 10
value of b in sum() = 20
value of c in main() = 30
初始化局部变量和全局变量
当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动对其初始化,如下所示:
正确地初始化变量是一个良好的编程习惯,否则有时候程序可能会产生意想不到的结果,因为未初始化的变量会导致一些在内存位置中已经可用的垃圾值。
12.1.3 链接
C变量有3种链接属性:外部链接、内部链接或无链接。
- 外部链接:可以在多文件程序使用
- 内部链接:只能在一个翻译单元(即一个源代码文件和它所包含的头文件)中使用
- 无链接:具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量
具有文件作用域的变量可以是外部链接或内部链接。
程序员用“内部链接的文件作用域”描述仅限于一个翻译单元的作用域,简称为“文件作用域”;而用“外部链接的文件作用域”描述可以延伸至其他翻译单元的作用域。,简称为“全局作用域”或“程序作用域”。
例如:
int giants = 5; // 文件作用域,外部链接
static int dodgers = 3; // 文件作用域,内部链接
int main()
{
...
}
该文件和同一程序的其他文件都可以使用变量giants,但是变量dodgers是私有的,只供该文件的任意函数使用。
12.1.4 存储期
作用域和链接描述了标识符的可见性。
存储期描述了通过这些标识符访问的对象的生存期。C对象有4种存储期:
- 静态存储期:文件作用域变量具有静态存储期,在程序的执行期间一直存在
- 线程存储期:用于并发程序设计,从被声明时到线程结束一直存在
- 自动存储期:块作用域的变量通常具有自动存储期
- 动态分配存储期
注:块作用域变量也能拥有静态存储期。创建这样的变量需要把它声明在块中,且加上关键字static。
12.1.5 C语言的5种存储类别
C使用作用域、链接和存储期为变量定义了5种存储类别。
自动变量auto
属于自动存储类别的变量具有自动存储期、块作用域且无链接。
关键字auto是存储类别说明符。
注:默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。
寄存器变量register
寄存器变量存储在CPU的寄存器(最快的可用内存)中,具有块作用域、无链接和自动存储期。
变量声明为register类别与直接命令相比更像是一种请求,编译器必须根据寄存器的数量衡量请求,或者直接忽略请求。
块作用域的静态变量static
块作用域的静态变量具有块作用域、无链接和静态存储期。
静态变量的静态指的是该变量在内存中原地不动,并不是它的值不变。
外部链接的静态变量extern
外部链接的静态变量具有文件作用域、外部链接和静态存储期。
使用关键字extern在该文件中声明这种变量。
内部链接的静态变量extern
内部链接的静态变量具有文件作用域、内部链接和静态存储期。
使用关键字extern在该文件中声明这种变量。
12.2 随机数函数——rand()
C 库函数 int rand(void) 返回一个范围在 0 到 RAND_MAX 之间的伪随机数。
RAND_MAX 是一个常量,它的默认值在不同的实现中会有所不同,但是值至少是 32767。
下面是 rand() 函数的声明:
int rand(void)
该函数返回一个范围在 0 到 RAND_MAX 之间的整数值。
实例:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
int i, n;
time_t t;
n = 5;
/* 初始化随机数发生器 */
srand((unsigned) time(&t));
/* 输出 0 到 49 之间的 5 个随机数 */
for( i = 0 ; i < n ; i++ ) {
printf("%d\n", rand() % 50);
}
return(0);
}
让我们编译并运行上面的程序,这将产生以下结果:
38
45
29
29
47
12.3 分配内存
C 语言为内存的分配和管理提供了几个函数。这些函数可以在 <stdlib.h> 头文件中找到。
注意:void * 类型表示未确定类型的指针。C、C++ 规定 void * 类型可以通过类型转换强制转换为任何其它类型的指针。
12.3.1 动态分配内存
实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char name[100];
char *description;
strcpy(name, "Zara Ali");
/* 动态分配内存 */
description = (char *)malloc( 200 * sizeof(char) );
if( description == NULL )
{
fprintf(stderr, "Error - unable to allocate required memory\n");
}
else
{
strcpy( description, "Zara ali a DPS student in class 10th");
}
printf("Name = %s\n", name );
printf("Description: %s\n", description );
}
当上面的代码被编译和执行时,它会产生下列结果:
Name = Zara Ali
Description: Zara ali a DPS student in class 10th
上面的程序也可以使用 calloc() 来编写,只需要把 malloc 替换为 calloc 即可,如下所示:
calloc(200, sizeof(char));
当动态分配内存时,程序员有完全控制权,可以传递任何大小的值。而那些预先定义了大小的数组,一旦定义则无法改变大小。
12.3.2 重新调整内存的大小
通过调用函数 realloc() 来增加或减少已分配的内存块的大小。
实例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char name[100];
char *description;
strcpy(name, "Zara Ali");
/* 动态分配内存 */
description = (char *)malloc( 30 * sizeof(char) );
if( description == NULL )
{
fprintf(stderr, "Error - unable to allocate required memory\n");
}
else
{
strcpy( description, "Zara ali a DPS student.");
}
/* 假设您想要存储更大的描述信息 */
description = (char *) realloc( description, 100 * sizeof(char) );
if( description == NULL )
{
fprintf(stderr, "Error - unable to allocate required memory\n");
}
else
{
strcat( description, "She is in class 10th");
}
printf("Name = %s\n", name );
printf("Description: %s\n", description );
/* 使用 free() 函数释放内存 */
free(description);
}
当上面的代码被编译和执行时,它会产生下列结果:
Name = Zara Ali
Description: Zara ali a DPS student.She is in class 10th
可以尝试一下不重新分配额外的内存,strcat() 函数会生成一个错误,因为存储 description 时可用的内存不足。
12.3.3 释放内存
调用函数 free() 来释放内存。
声明:
void free(void *address);
该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。
malloc()或calloc()要与free()配套使用,否则可能造成内存泄漏。
12.4 ANSI C类型限定符
我们通常用类型和存储类别来描述一个变量。
12.4.1 const类型限定符
以const关键字声明的对象,其值不能通过赋值或递增、递减来修改。
12.4.2 volatile类型限定符
volatile限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值。
12.4.3 restrict类型限定符
restrict关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。
12.4.4 _Atomic类型限定符
_Atomic关键字声明一个原子类型的变量,当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象。