个性化阅读
专注于IT技术分析

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 = 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、块作用域的静态变量

这里要特别说一下静态变量,先不要和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为引用声明,因为定义声明是唯一确定了一个变量在内存中的具体形式了,所以引用声明中就不能再次进行声明赋值而只能进行引用声明:

外部变量的定义声明和引用声明

像上面说到的,.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,数组元素初始值为0

void 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 = 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 = 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]);
赞(0) 打赏
未经允许不得转载:srcmini » C语言简明教程(十四):存储类别、链接、内存管理和类型限定
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!

 

觉得文章有用就打赏一下文章作者

微信扫一扫打赏