后面还有6章,12-14章内容比较重要,有很多语法需要掌握,15-17章的内容相对不重要,多是简要了解即可。
全书共分17章,这是关于本书第12章内容的博客。本章主要介绍了关于存储类别和动态分配内存的内容。博客的目录和书上目录是相似的。此系列博客的代码都在Visual Studio 2022环境下编译运行。
我目前大一刚刚结束,水平有限,博客中若有错误或者总结不到位的地方也请见谅。
目录
12.1 存储类别
C语言提供多种存储类别在内存中存储数据。
程序中使用的数据存储在内存中,每个值都占用一定的内存,C语言把这样的一块内存称为对象。对象可以存储一个或多个值,一个对象可能未存储实际的值,但是它在存储适当的值时一定具有相应的大小。(C++,Java等面向对象的编程语言中,对象指的是类对象,定义包括数据和允许对数据进行的操作,C语言是面向过程的编程语言)。
程序需要访问对象,可以通过声明变量完成。声明创建了标识符,标识符是一个名称,可以指定特定对象的内容。指针指定了一个存储地址的对象。
可以用存储期描述对象,存储期指对象在内存中保留了多长时间。标识符的作用域和链接表明了程序的哪些部分可以使用它。不同的存储类别具有不同的存储期、作用域和链接。
12.1.1 作用域
作用域表述程序中可访问标识符的区域,作用域有块作用域,函数作用域、函数原型作用域或文件作用域。
块是用花括号括起来的代码区域,定义在块中的变量具有块作用域。块作用域变量的可见范围是定义处到包含该定义的块末尾。局部变量和形式参数都具有块作用域。声明在内层块中的变量作用域局限于此内存块。
for循环、while循环、do while循环、if语句控制的代码也都是块,即使可能没有用花括号括起来。
函数作用域仅用于goto语句的标签。
函数原型作用域用于函数原型中的形参名,范围是形参定义到原型声明结束。
变量定义在函数外面,具有文件作用域。具有文件作用域的变量从定义处到定义所在文件的末尾均可见。文件作用域变量也称为全局变量。
头文件可能包含其他文件,所以会包含多个文件。C预处理器实际上用包含的头文件替换#include指令。编译器把源代码文件和所有的头文件看作一个包含信息的单独文件,这个文件被称为翻译单元。全局变量的实际可见范围是翻译单元,多个源代码文件有多个翻译单元。
12.1.2 链接
C语言的变量有三种链接属性:外部链接、内部链接和无链接。
具有块作用域、函数作用域或函数原型作用域的变量是无链接变量,属于定义它们的块、函数或原型私有。
具有文件作用域的变量可以是外部链接,也可以是内部链接。外部链接变量可以在多文件程序中使用,内部链接变量只能在一个翻译单元中使用。
全局变量定义中有static的为内部链接,否则为外部链接。
12.1.3 存储期
作用域和链接描述了标识符的可见性,存储期描述了通过这些标识符访问对象的生存期。C语言有四种存储期:静态存储期、线程存储期、自动存储期和动态分配存储期。
具有静态存储期的变量在程序执行期间一直存在,文件作用域变量具有静态存储期。static表明链接属性,而非存储期。内部链接和外部链接都具有静态存储期。
线程存储期用于并发程序设计。程序执行可以分为多个线程,具有线程存储期的对象从被声明到线程结束一直存在。本书不涉及并发程序设计。
块作用域的变量通常具有自动存储期,存储期从块的开始处到块的结尾(变长数组是声明处到块的末尾),局部变量是自动类别。
块作用域的变量可能有静态存储期,这种情况要把变量声明在块中,前面加上static。
动态分配在后面会有介绍。
12.1.4 自动变量
自动存储类别的变量具有自动存储期、块作用域且无链接。声明在块或函数头中的变量都是自动存储类别。可以在定义前显式使用关键字auto。
只有在变量定义的块中才能通过变量名访问该变量(传参是间接的方法)。
自动存储期的变量在程序进入定义所在块时存在,在退出时消失。
嵌套块中内部块的变量仅限于该块使用。如果内部块与外部块有变量重名,内层块会隐藏外部块中同名变量的定义。
循环体是所在循环块的子块,if语句内的块是if语句的子块。
自动变量不会自动初始化,除非显式初始化,如果不显式初始化,值为任意值。
12.1.5 寄存器变量
如果变量存储在CPU的寄存器中,访问和处理这个变量的速度更快,但是无法获取此变量的地址。
寄存器变量也是块作用域、无链接和自动存储期。
使用register声明寄存器变量。即使用register声明,也不一定会放在寄存器中,这时变量是普通的自动变量,但是仍然不能获取地址。
12.1.6 块作用域的静态变量
静态变量是变量在内存中不变的变量。静态变量在程序离开所在函数后不会消失,会维持原值不变。静态变量无链接、具有块作用域和静态存储期。
用static声明静态变量。
下面代码演示了函数中局部变量和静态变量的区别。
#include<stdio.h>
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;
printf("fade = %d and stay = %d\n", fade++, stay++);
}
12.1.7 外部链接的静态变量
外部链接的静态变量具有文件作用域、外部链接和静态存储期,属于该类别的变量称为外部变量。
外部变量定义在所有函数的外边。为了指出某函数使用了外部变量,可以在函数中使用关键字extern再次声明(如果函数在定义下面,就可以不写,但是如果声明中没有extern,就相当于创建了一个自动变量)。但是如果一个源代码文件使用的外部变量定义在另一个源代码文件中,就必须用extern在该文件中再次声明。
执行块中的语句时,块作用域的变量会隐藏文件作用域中的同名变量。
外部变量的作用域是从声明处到文件结尾。如果未初始化外部变量,则会被自动初始化为0。只能用常量表达式初始化文件作用域变量。
下面是一个使用外部变量的例子。
#include<stdio.h>
int units = 0;
void critic(void);
int main(void)
{
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);
}
关键字extern表明该声明不是定义,指示编译器去别处查找定义。不要用extern创建外部定义。
外部变量只能初始化一次,必须在定义时进行。
12.1.8 内部链接的静态变量
内部链接的静态变量具有静态存储期、文件作用域和内部链接。在所有函数的外部用static定义。
普通的外部变量可用于同一程序中任意文件,但是内部链接的静态变量只能用于同一文件内。也可以使用extern在此文件内的函数中声明。
12.1.9 多文件
如果多个源代码文件要共享一个外部变量,一般在一个文件中进行定义,其他文件中用extern声明。
12.1.10 存储类别说明符
C语言有6个关键字作为存储类别说明符:auto,register,static,extern,_Thread_local,typedef。
auto表明变量是自动存储期,只能用于块作用域的变量说明。register只用于块作用域的变量,表明变量为寄存器存储类别。static表明变量有静态存储期,用于文件作用域则作用域受限于该文件,用于块作用域则表明作用域受限于该块。extern表明声明的变量定义在别处。
12.1.11 存储类别和函数
函数也有存储类别,默认是外部函数,即可以被其他文件的函数访问的函数。也可以是静态函数,只能用于定义所在的文件,用static修饰。extern可以声明函数定义在其他文件中。
12.1.12 存储类别的选择
一般使用自动存储类别,因为外部变量可能被函数意外修改。
const修饰的变量在初始化后不会被修改。
尽量在函数内部解决该函数的任务,只共享需要共享的变量。
12.2 随机数函数和静态变量
C库提供了rand()函数生成随机数。生成随机数有多种算法,C库提供了标准算法。rand()实际是伪随机数生成器,可预测生成数字的实际序列,容易导致输出很接近。可以用srand()函数使rand()函数生成数字比较接近随机数的数。
rand()函数没有参数,srand()函数有一个参数,是一个整数,用来修改rand()函数生成的随机序列。这两个函数定义在stdlib.h头文件中。
可以用可变的量作为srand()函数的参数,比如时间。time.h头文件提供了time()函数返回系统时间。返回值类型名是time_t。time函数有一个参数,接受一个time_t类型地址,可以用空指针作为参数。
可以用相关知识做一个小游戏,如掷色子。
12.3 分配内存:malloc()和free()
C语言可以使用库函数分配和管理内存。
malloc()函数接受一个参数:所需的字节数。malloc()函数会找到合适的空闲内存块(匿名的,不会赋名),并返回此内存块的首字节地址。返回值是void*类型指针,可以通过强制类型转换赋给匹配的指针变量,用指针访问内存。
如果分配失败,会返回空指针。
free()函数释放malloc()分配的内存。动态内存分配的存储期从调用malloc()到free()为止。free()的参数是一个指针,指向动态分配的内存,不能释放指向普通变量的指针。
malloc()函数和free()函数定义在stdlib.h头文件中。
exit()函数可以结束程序,原型在stdlib.h中。
下面程序用动态内存分配创建了一个数组。
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
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));
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 entries:\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;
}
free()释放参数指向的内存块,一些操作系统会在程序结束时自动释放动态分配内存,一些系统不会。
动态内存分配使程序更灵活。
12.3.1 free()的重要性
静态内存的数量是固定的,运行期间不会改变。自动变量使用的内存数量在执行期间会增加或减少。动态分配的内存数量只会增加,除非用free()释放。
如果忘记free(),可能消耗太多内存空间,会造成内存泄漏。
12.3.2 calloc()函数
分配内存也可以使用calloc()函数,calloc()函数有两个参数,第一个是所需单元的数量,第二个是单元的大小,返回类型为void*。
calloc()函数把块中的所有位都设置为0,而malloc()不会。free()也可释放calloc()分配的内存。
动态内存分配是很多高级程序设计的关键。
12.3.3 动态内存分配和变长数组
变长数组和动态分配的数组都可用于创建运行时确定大小的数组。
变长数组是自动存储类型。
不能多次用free()释放同一块内存空间。
多维数组用变长数组更方便,也可以进行动态内存分配。
12.3.4 存储类别和动态内存分配
程序把静态对象、自动对象(称为栈)和动态分配的对象(称为堆)存储在不同的区域。
12.4 ANSI C类型限定符
通常用类型和存储类别描述一个变量。
C90新增了两个属性:恒常性和易变性。分别用关键字const和volatile声明。C99新增了限定符restrict,用于提高编译器优化。C11新增了限定符_Atomic。C11提供了一个可选库,由stdatomic.h管理,以支持并发程序设计。
C99为类型限定符增加了一个新属性:幂等性。即一条声明中多次使用同一个限定符会忽略多余的限定符。
12.4.1 const类型限定符
const关键字声明的对象不能被修改,可以初始化。
用const修饰指针时,如果const位于类型的左边,或位于类型名和*之间,则此指针不能改变指向变量的值,但可以改变指向的对象。const放在*和指针名的中间,则不能改变指向的对象,可以改变指向变量的值。
对全局变量使用const可以防止值被意外修改。
在文件中共享const时,可以在定义的文件中用const,其他文件用extern const。也可以把const变量放在头文件中,由其他源文件包含头文件,此时必须用static声明const全局变量,否则可能引起冲突。相当于给每个文件提供了单独的副本。
12.4.2 volatile类型限定符
volatile限定符告诉计算机,代理可以改变该变量的值。通常用于硬件地址以及在其他程序或同时运行的线程中共享数据。
volatile涉及编译器的优化。
12.4.3 restrict类型限定符
restrict关键字允许编译器优化某部分代码以更好地支持运算。只能用于指针表明该指针是访问数据对象的唯一且初始的方式。restrict可用于函数形参中的指针表明编译器可以假定函数体的其他标识符不会修改该指针指向的数据。
12.4.4 _Atomic类型限定符(C11)
并发程序设计把程序执行分成可以同时执行的多个线程。C11通过stdatomic.h和threads.h提供了一些可选的管理方法。
12.4.5 旧关键字的新位置
C99允许把类型限定符和存储类别说明符static放在函数原型和函数头形式参数的初始方括号中,用法是在名称后面加方括号,然后关键字放在方括号中。
新标准为static增加了不同的用法,可以告知编译器如何使用形参。