C语言简明教程(十四)(存储类别、链接、内存管理和类型限定)

接上一节字符串和字符串处理函数实例详解
本节对于C语言编程的理解十分重要,例如对C程序的执行逻辑的理解、对存储类别的选择和使用、malloc内存分配其实只是存储类别中的一种、如何合理设计程序的代码以及各源文件和头文件的结构等等。变量是一种储存在内存中的数据对象,C语言的变量类型除了基本类型还有数组类型、函数类型(也有函数变量)、结构体类型、枚举类型、联合类型,变量的一个显著的特点是在内存中占有一块内存,本节主要围绕变量和内存进行讨论,变量使用存储类别描述,下面先详细讨论存储类别对变量描述的具体分类原理或分类依据。

C语言简明教程(十四)(存储类别、链接、内存管理和类型限定)

文章图片
一、存储类别分类原理或分类依据数据对象或简称为对象,占有一块或多块物理内存,可以存储一个或多个数据值,例如int a对象的内存块存储一个4字节32位整数值,char arr[10]连续存储10个char类型的字符,每块内存大小为1字节8位。标识符用于在软件层面指定内存对象,标识符直接指数据对象,但是标识符并不是指变量,还有宏定义,宏定义在预编译的时候会进行宏替换,所以严格来说宏不是一个变量,我们编程的大部分都是使用变量,可以使用作用域、链接和存储期,作用域和链接是用于描述变量的可被访问的区域,存储期是用于描述变量在内存中的存储时间。
1、作用域
作用域指的是标识符可被访问的有效区域或作用范围,作用域类似于数学中的定义域,例如O={x | a < x < b},a为定义域开始,b为定义域结束,C语言中的作用域可分为如下的作用域:
(1)块作用域,双括号括起来的内容,变量的块作用域从定义开始到包含该定义的块末尾,作用域开始:定义开始处,作用域结束:定义结束处,示例:
// 1、普通块作用域 { // 块作用域 int number = 9; // number变量作用域开始 printf("%d\n", number); } // number变量作用域结束// 2、for循环块作用域 for (int i = 0; i < 10; ++i) { // 变量i属于for块作用域 printf("%d\n", i); } // i变量作用域结束// 3、if块作用域 if(1){ int value = http://www.srcmini.com/8; // value定义开始 printf("%d\n", value); } // value作用域结束// 4、while块作用域 while(1){}// 5、do while块作用域 do{}while (1)

(2)函数作用域,仅限于goto语句的标签,一个goto标签的作用域就是整个函数,示例:
printf("%ld\n", time(NULL)); goto label; // 执行goto标签开始的语句 label: // goto标签,该标签的作用域为run函数作用域,其它函数可以使用相同名称的label标签 printf("hello\n");

(3)函数原型作用域,作用域开始:形参定义处,作用域结束:原型声明结束。
(4)文件作用域,文件作用域指的是在所有函数外部,文件作用域变量又称为全局变量,变量定义域开始:变量定义处,定义域结束:文件结束。一个文件作用域 又称为一个编译单元,包含头文件,因为头文件中的内容会在预处理的时候进行头文件替换,C程序一般包含多个源文件或多个编译单元,两个源文件之间不能存在两个相同名字的外链接变量。这里要注意,头文件(.h)和源文件(.c)都是源文件,编译的时候.c文件都将头文件包含进去了,所以不用编译头文件,习惯上头文件用于类型声明,如果在里面定义一个外链接全局变量,那么就会造成冲突,文件作用域示例:
// 文件作用域开始,从文件的开头开始#include "pro_09.h" #include < stdio.h> #include < stdlib.h> #include < time.h> #include < string.h>int a = 9; // 变量a作用域开始,结束处为文件结尾(外链接静态变量) static int count = 90; // 变量count作用域开始,结束处为文件结尾(内链接静态变量)extern void running(void);

2、链接
链接同样是描述变量的作用范围的,主要是针对文件作用域上的变量,描述编译单元中的变量可被链接访问的程度。块作用域内的变量无链接,还包括函数作用域和函数原型作用域,它们都是无链接的。
外部链接:可在多个源文件之间使用,外部链接文件作用域也称为全局作用域,外部链接变量为非static变量。
内部链接:仅仅在一个编译单元内使用,内部链接文件作用域又称为文件作用域,文件作用域内部链接变量为static变量,具体示例代码如下(代码在所有函数外部):
int rain = 333; // 文件作用域,外链接,可以在所有文件中使用 static int cloud = 111; // 文件作用域,内链接,只能在本文件内使用

3、存储期
存储期是描述数据对象在内存中存在的时间,C语言的存储期有4种:
存储期 说明
静态存储期 1、静态存储期的变量在程序执行期间一直存在。 2、静态存储期的变量包括文件作用域变量和块作用域中的静态变量 3、这些变量保存在静态内存区,文字常量一般不可修改,部分类型的静态变量可修改。
线程存储期 这种变量为当前线程的变量,获取变量的私有副本,这些变量一般有私有的栈内存,主要是解决线程安全的问题。
自动存储期 变量空间自动分配自动释放,这些变量一般是块作用域变量,保存在栈内存中。
动态分配存储期 这些变量保存在堆内存中,由程序员手动分配(使用malloc、calloc或realloc),手动释放(free)
对变量的认识主要还是要结合程序内存结构,每种变量对应于内存的某一分区,自动变量对应于栈内存,动态变量对应于堆内存,静态变量对应于静态区,C语言的内存分区如下图:
C语言简明教程(十四)(存储类别、链接、内存管理和类型限定)

文章图片
二、存储类别判别存储类别的依据是使用上面讨论到的作用域、链接和存储期,作用域主要集中在块作用域,而链接分为内链接和外链接,均发生在文件作用域,存储期则是核心的程序内存分布区分,集中在堆区、栈区和静态区,下面详细介绍C语言的存储类别。
1、自动变量
自动变量具有自动存储期、块作用域和无链接,也就是说自动变量存储在栈内存中,进入块执行时变量被创建,退出块时被释放,使用auto关键字显示声明变量为自动存储而不是其它存储类别,一般块作用域内声明的变量默认为自动变量。自动变量若为初始化,则值随机未知。注意,如果存在外块和内块变量同名,则执行到内块时隐藏外块的变量定义,不过主要还是根据栈的结构分析,先入栈的先被访问到,后入栈的后被访问到,自动变量的示例代码和分析如下:
// 自动变量在进入函数块时创建 auto int position = 77; // 显式声明为自动变量,使用auto关键字 int number = 99; // 默认为自动变量 { // 普通块作用域 printf("%d\n", number); // 输出:99,此时还是按照占内存的结构使用变量,外块变量先入栈先被使用 auto int number = 22; // 暂时隐藏外块的同名变量number,从这里开始使用内块定义的自动变量 printf("%d\n", number); // 输出:22 } printf("%d\n", number); // 输出:99 // 自动变量在退出函数块时释放

2、寄存器变量
寄存器变量具有自动存储期、块作用域和无链接,使用register关键字声明,跟上面的自动变量类似,但是寄存器变量存储在寄存器中,寄存器的空间有限,不过处理速度较快,所以太大的数据块不适宜使用寄存器变量存储。注意,寄存器变量有些特殊,编译器只是尽可能地将声明为寄存器变量的数据存储在寄存器中,并不是一定存储在寄存器中,另外,寄存器变量禁止被寻址,如果寄存器变量不存储在寄存器中则自动变为自动变量,此时仍然不能被寻址。
register int index = 9; // int *pt = &index; // 编译器报错,寄存器变量不能被寻址 // 寄存器变量和自动变量的使用类似 { printf("%d\n", index); // 输出:9 register int index = 1; printf("%d\n", index); // 输出:1 } printf("%d\n", index); // 输出:9

3、块作用域的静态变量
【C语言简明教程(十四)(存储类别、链接、内存管理和类型限定)】这里要特别说一下静态变量,先不要和static关系起来,准确来说和static关系不大,因为静态变量的准确定义是:存储在静态区的变量则为静态变量,例如文件作用域的非static变量也是静态变量,并不是static关键字决定的,静态变量值可变,程序执行期间一直保存在内存中。块作用域的静态变量具有静态存储期、块作用域和无链接,也称为内部静态存储类别,块作用域的静态变量需要使用static关键字声明(可以看到static关键字的意义并没有那么明了),static不能用作形参,静态变量只会在编译时初始化一次,之后不会重新初始化,无初始化默认初始化为0,示例代码和分析如下:
void increase(void){ // 静态变量存储在静态内存中,程序执行期间一直存在 static int counter = 1; printf("%d\n", counter); counter++; }void print(void){ static int count = 1; // 编译时初始化为1 static int index; // 默认初始化为0 printf("%d\n", count); // 输出:1 printf("%d\n", index); // 输出:0 increase(); // 输出:1 increase(); // 输出:2 increase(); // 输出:3 }

4、外部链接的静态变量
外部链接的静态变量具有静态存储期、文件作用域和外链接,又称为外部变量,主要特点是定义在所有函数的外部,为文件作用域变量,具有外链接则可以供程序所有文件共享。外链接变量首先需要有变量的实际定义声明,另外使用需要有引用声明,定义声明指的是变量在内存中的具体数据形式,就是已经存在内存中的了,引用声明则只是声明引用一个外部变量,使用extern关键字进行引用声明,告诉编译器到寻找变量的原始定义声明,例如,int number = 9为定义式声明,而extern int number为引用声明,因为定义声明是唯一确定了一个变量在内存中的具体形式了,所以引用声明中就不能再次进行声明赋值而只能进行引用声明:
C语言简明教程(十四)(存储类别、链接、内存管理和类型限定)

文章图片
像上面说到的,.h头文件和.c源文件同是源文件,简单地称它们为文件,按照设计惯例,头文件用于接口声明,.c文件用于实现,从C语言的角度看,.c文件中的函数实现才是函数变量的具体定义,即在这里的函数才是真正的内存中的函数变量,头文件中函数原型声明则是引用声明,在这里你可以看到.c文件一般不会被重新包含,因为这里是变量的定义或实现,而头文件一般是引用声明,可以多次使用引用声明,引用声明只是告诉编译器到其它地方寻找实际定义,并没有占用数据内存空间,因为一般的函数变量其实默认也是静态变量[非static](文件作用域、静态存储、外链接),所以C函数默认都是全局变量(一般函数或函数外部的变量都是属于外部变量),一般来说在头文件.h对应的.c文件中并不需要包含头文件,因为编译器会自动寻找.c文件的实现,这里可以导出头文件的本质:在使用函数时,更方便地一次性进行模块中所有函数的引用声明,实际上你也可以自己手动进行extern引用声明。
外部变量只能使用常量表达式初始化,不能使用变量初始化,若未初始化则自动初始化为0,注意在extern引用声明中,数组变量不能重新指定数组大小,外部变量不能同名,函数原型的默认声明为extern,下面是具体的代码示例和分析:
// 函数原型声明默认为extern,可以在头文件中进行显式声明,编译器会自动寻找draw函数的实际定义 extern void draw(void); // time()函数原型一般在time.h头文件中,可以不包含手动进行声明,但是一次包含头文件更方便 extern time_t time(time_t *); extern time_t time(time_t *); // 可被重新进行引用声明// 定义外部变量 int ftime = 9; int farr[10]; // 未初始化,默认初始化为0,数组元素初始值为0void print_09_05(void){ extern int ftime; // 使用extern使用外部变量 extern int farr[]; // extern引用声明不用重新指定数组大小 ftime = 900; printf("%d\n", ftime); printf("%d %d %d\n", farr[0], farr[1], farr[2]); // 输出:0 0 0 }

5、内部链接的静态变量
使用static关键字声明,具有静态存储期、文件作用域和内部链接,可以使用extern重新引用声明,不过内部链接的静态变量仅限于本文件使用,所以一般也可省略引用声明,代码示例如下:
// 定义一个内部链接静态变量,使用static关键字修饰,仅限于本文件使用 static int bite = 32; void print(void){ // 使用extern引用声明 extern int bite; printf("%d\n", bite); }

6、_Thread_local线程变量
_Thread_local修饰的变量用于解决线程安全的问题,多个线程共享一个数据对象,该数据对象可以使用_Thread_local修饰,_Thread_local可以和static或extern一起使用。
7、动态变量与内存管理
变量的存储类别使用对应程序的内存分区,而不只是根据关键字判断,动态变量没有关键字,它是存储在堆内存中的,堆内存属于自由内存,也就是由程序员自由申请自由释放,这种变量称为动态变量。
C的头文件stdlib.h提供alloc函数分配内存:malloc内存分配、calloc按数量分配和realloc重新分配,alloc函数返回需要使用强制类型转换,分配失败返回NULL空指针。
内存分配需要的基本量是储存单元数量和存储单元的大小,其中malloc不会对分配到的内存进行初始化操作,而calloc则会将存储单元初始化为0,使用完内存后需要使用free函数手动释放内存,忘记free会造成内存泄漏,即该被释放的内存没有被释放。
内存分配失败或打开文件失败等情况,一般程序不再继续运行,这时可使用exit函数退出程序执行,exit函数的参数有两种情况:EXIT_FAILURE程序异常终止,EXIT_SUCCESS程序正常结束,下面是使用alloc函数分配内存的示例代码:
// 分配内存的基本元素:储存单元数量、储存单元的大小 // 1、使用malloc函数分配内存,默认不初始化内存空间 int *numbers = (int *)malloc(10 * sizeof(int)); // 储存单元数量为10,储存单元的大小为sizeof(int) printf("%d\n", numbers[0]); // 输出随机,不确定,这里输出为:6559360 memset(numbers, 0, 10 * sizeof(int)); // 初始化所有内存空间值为0 printf("%d\n", numbers[0]); // 初始化后输出为:0 numbers[0] = 999; // 所分配的内存是连续的内存空间,可以使用数组的方式访问 printf("%d\n", numbers[0]); // 2、使用calloc函数分配内存,内存空间初始化为0 int *array = (int *)calloc(10, sizeof(int)); printf("%d\n", *array); // 使用指针访问内存数据对象,初始化值为0 *array = 666; *(array + 1) = 777; printf("%d %d\n", *array, *(array + 1)); // 3、使用realloc函数重新分配内存,该函数是根据原来的内存空间进行扩展或收缩 // 如果原来的内存空间不足则自动寻找更大的空间,将数据复制到新空间上;如果原来的空间后面还可以扩展,返回的指针则不变 // 注意如果获得新的内存空间,则会自动释放掉原来的内存空间,原来的内存不需要再手动释放 // 如果原来的空间足够,则不再分配,返回原来空间的首地址 int *langs = (int *)malloc(2 * sizeof(int)); if(!langs){ exit(EXIT_FAILURE); } *langs = 111; *(langs + 1) = 222; printf("%p\n", langs); // 原内存地址:003816B0 langs = (int *)realloc(langs, 10000); printf("%p\n", langs); // 新分配的内存地址:00383FC8 *(langs + 2) = 333; printf("%d %d %d\n", *langs, *(langs + 1), *(langs + 2)); free(langs);

8、重点说明
typedef不能和多数存储类别一起使用,外部函数原型默认为extern声明,内部函数使用static函数声明,内联函数使用inline声明。之前几节已经详细讨论过函数和指针以及数组,它们都是一种数据类型,也就是有对应类型的变量,不管是一般类型的变量还是特殊类型的变量,在C语言多文件编译的时候,多个编译单元的变量共享使用extern作引用声明,文件包含要注意,引用声明可以重复包含,因为引用声明不占实际变量的内存空间,但是变量定义不可以重复(除非是static),因为变量已经分配内存空间。
头文件一般作接口声明,也就是引用声明,或使用extern显式作引用声明,如果要作变量定义声明则需要使用static,否则多文件包含就会造成变量重复,编译肯定出错,即使是宏定义也要注意重名的问题。.c源文件一般是接口的实现,这里是变量或函数的实际定义,所以一般不会去包含.c源文件的,因为多个包含直接就有冲突了。所以原则是重复使用引用声明,不重复包含定义声明(注意一般头文件是防止同文件重复包含,但是不同文件仍然需要包含)。
三、类型限定符C的类型限定符是用于额外修饰变量的,但不是很正式或很严重的限定,例如有些编译器const变量修改数据对象可能没问题,不过既然要使用标准的类型限定符就要按照标准的用法使用,C99添加一个幂等的特性,即一条声明语句中使用多个相同的限定符,则忽略多余限定符,仅保留一个,下面详细介绍C的类型限定符。
1、const类型限定符
C90标准添加的修饰符,用于修饰变量,表示该变量对应的数据对象不可修改,但可读,修饰数组,则数组的数据值不可变。Const用在形参中表示不允许修改传入的数据对象,const类型限定有以下几种特殊情况:
(1)在指针中的const,位于*左边的const表示指针指向的数据对象不可变,位于*右边的const表示指针值不可变。例如const int *pt和int const *pt均表示pt指向的数据对象不可变,int * cosnt pt表示pt其值不可变。
(2)全局数据使用const,即const外部变量,上面已经说到,其核心在于这是一个存在内存中的变量,所以不能被重复包含,如需使用需要使用static限制为内链接。
const的用法和示例代码如下:
// 全局const内链接变量 static const int data = http://www.srcmini.com/900; void print(void){ // 1、const修饰普通变量,称为const常量 const int EXP = 2; int * pep = &EXP; *pep = 20; // 这里允许修改,但是别这么做 printf("%p %d\n", pep, EXP); // 2、const修饰数组 const char* langs[2] = {"JavaScript", "C++"}; langs[0] = "Java"; printf("%s\n", langs[0]); // 3、const修饰指针所指向的数据对象 const char *pt = "PT"; char const *pp = "PP"; // 4、const修饰指针本身 int number = 9; int * const lpt = &number; }

2、volatile类型限定符
C90添加的关键字,表示变量的异变性,使用volatile修饰的变量表明该变量在程序运行过程中可能会被其它代理改变,主要作用是和其它线程或其它进程共享一个数据对象。例如int n = 9; int v01 = n; int v02 = n; 将n的值保存在寄存器中,给v02赋值的时候,将寄存器中的值赋值给v02,但是如果使用volatile修饰变量,则02赋值的时候直接从内存中获取,如果过程中有其它进程修改该变量值,可以保证数据同步。
// volatile变量一般使用const修饰,表示本程序不允许修改该变量,仅允许使用代理修改 const volatile int sound = 99; printf("%d\n", sound);

3、restrict类型限定符
restrict主要用于修饰指针,它仅仅是提供给编译器优化的标识,表明该指针是访问数据对象的唯一且初始的方式,即不存在第二个指针访问同一个对象,但是仍然不是严格的修饰符(你仍然可以使用第二个指针)。restrict用在形参中表示一种传参规范,也就是参数不存在重叠的情况,比起指针访问的唯一性,表示传参规范更有用。
// restrict修饰符表示指针是唯一访问且最初该对象的方式 int * restrict langs = (int *)malloc(sizeof(int)); *langs = 99; // restrict在形参中修饰,表示dest和src不建议使用同一个数据对象 extern void print(char *restrict dest, char *restrict src);

4、_Atomic类型限定符
C11添加的关键字,用于并发编程,但不是所有编译器都支持该关键字,C并发编程使用stdatomic.h和threads.h,_Atomic原子类型主要是用于解决线程安全,一个线程访问_Atomic数据,另一个线程不能同时访问该数据对象。
// _Atomic原子类型,修饰变量为线程共享对象 _Atomic int data = http://www.srcmini.com/88;

5、限定符和static的新写法
C99允许在函数原型和函数头中使用,但是这种新写法并不是很友好,它增加了原关键字的语义,甚至会显得晦涩难懂,写法上有点反人类,不建议使用新写法。新写法主要体现在指针修饰和static优化,例如:
const指针修饰,int *const pt可以写成 int pt[const]。
restrict指针修饰,int *restrict pt可以写成int pt[restrict]。
static优化,告知编译器如何使用参数,如int pt[static 10],告知编译器如何使用参数,数组pt至少有10个元素。
// const修饰指针的新用法 // static优化的新用法 void login(char [const], char email[restrict]); void logout(char [static 10], const char message[const]);

    推荐阅读