7、存储类别、链接和内存管理

存储类别

存储类别的概念和术语

C提供了多种不同的模型或存储类别(storage class)在内存中储存数据。要理解这些存储类别,先要知道一些概念和术语。

  • 从硬件方面来看,被储存的每个值都占用一定的物理内存,C语言把这样的一块内存称为对象(obiec)。对象可以储存一个或多个值。一个对象可能并未储存实际的值,但是它在储存适当的值时一定具有相应的大小

  • 从软件方面来看,程序需要一种方法访问对象。这可以通过声明变量来完成,也就是标识符。用来访问对象

  • 可以用存储期(siorage duration)描述对象,所谓存储期是指对象在内存中保留了多长时间。

  • 标识符用于访问对象,可以用作用域(scope)链接(inkage)描述标识符,标识符的作用域和链接表明了程序的哪些部分可以使用它。

不同的存储类别具有不同的存储期、作用域和链接。

作用域

作用域描述程序中可访问标识符的区域。一个C变量的作用域可以是块作用域、函数作用域、函数原型作用域和文件作用域。

块作用域

块是用一对花括号括起来的代码区域。

定义在块中的变量具有块作用域(block scope),块作用域变量的可见范围是从定义处到包含该定义的块的末尾。

  • 虽然函数的形式参数声明在函数的左花括号之前,但是它们也具有块作用域,属于函数体这个块。例如如下函数

    int blocky(int cleo)	// cleo具有块作用域,属于blocky函数这个块
    {
        int patrick = 2;	// patrick具有块作用域,属于blocky函数这个块
        ...
        return patrick;
    }
    
  • 整个函数体是一个块,函数中的任意复合语句也是一个块。

    int blocky(int cleo)
    {
        int patrick = 2;
        for(int i = 0;i < 5; i++)
        {
            int q = cleo * i;	// q的作用域和i的作用域开始
            patrick += q;
        }	// q的作用域和i的作用域结束
        return patrick;
    }
    

    q的作用域和 i 的作用域仅限于内层块,只有内层块中的代码才能访问 q 和 i 。

    以前,具有块作用域的变量都必须声明在块的开头。C99标准放宽了这一限制,允许在块中的任意位置声明变量。

    为适应这个新特性,C99把块的概念扩展到包括for循环、while循环、do while 循环和 if 语句所控制的代码,即使这些代码没有用花括号括起来,也算是块的一部分。所以,上面for循环中的变量i被视为 for 循环块的一部分,它的作用域仅限于for 循环。一旦程序离开 for 循环,就不能再访问 i。

函数作用域

函数作用域(function scope)仅用于 goto语句的标签。这意味着即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数。如果在两个块中使用相同的标签会很混乱,标签的函数作用域防止了这样的事情发生。

函数原型作用域

函数原型作用域(function prototype scope)用于函数原型中的形参名(变量名),如下所示:

int mighty(int mouse, double large);

函数原型作用域的范围是从形参定义处到原型声明结束。这意味着,编译器在处理函数原型中的形参时只关心它的类型,而形参名(如果有的话)通常无关紧要。而且,即使有形参名,也不必与函数定义中的形参名相匹配。

只有在变长数组中,形参名才有用:

void use a VLA(int n,int m,ar[n][m]);

方括号中必须使用在函数原型中已声明的名称。

文件作用域

变量的定义在函数的外面,具有文件作用域(lescope)。具有文件作用域的变量,从它的定义处到该定义所在文件的末尾均可见。如下例子

#include <stdio.h>
int count = 0;	// 该变量具有文件作用域

int main()
{
    ...
}

void critic()
{
    ...
}

这里,变量count具有文件作用域(准确的说具有外部链接的文件作用域,后面有详解),main()和critic()函数都可以使用。由于这样的变量可以被多个函数使用,所以有文件作用域的变量也叫全局变量(global variable)

链接

C变量有3种链接属性:外部链接、内部链接或无链接。

  • 具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。这意味着这些变量属于定义它们的块、函数或原型私有。

  • 具有文件作用域的变量可以是外部链接或内部链接。

    • 外部链接变量可以在多文件程序中使用
    • 内部链接变量只能在一个翻译单元中使用。

想要知道文件作用域变量是内部还是外部链接,可以查看外部定义中是否使用了存储类别说明符:static

#include <stdio.h>

int giants = 4;				// 文件作用域,外部链接
static int dodgers = 3;		// 文件作用域,内部链接

int main()
{
    ...
}

该文件和同一程序的其他文件都可以使用变量 giants。而变量 dodgers 属翻译单元私有,只有该翻译单元中的任意函数可使用他。

翻译单元和文件

通常在源代码(.c扩展名)中包含一个或多个头文件(.h 扩展名)。头文件会依次包含其他头文件,所以会包含多个单独的物理文件。

但是,C预处理实际上是用包含的头文件内容替换#include 指令。所以,编译器源代码文件和他所包含的所有的头文件都看成是一个包含信息的单独文件。这个文件被称为翻译单元(translalion uni )

描述一个具有文件作用域的变量时,它的实际可见范围是整个翻译单元。如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成。每个翻译单元均对应一个源代码文件和它所包含的文件。

存储期

作用域和链接描述了标识符的可见性。存储期描述了通过这些标识符访问的对象的生存期。

C对象有 4种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。

  • 如果对象具有静态存储期,那么它在程序的执行期间一直存在。文件作用域变量一定具有静态存储期。块作用域也有静态存储期。

    注意,对于文件作用域变量,关键字static表明了其链接属性,而非存储期。以static声明的文件作用域变量具有内部链接。但是无论是内部链接还是外部链接,所有的文件作用域变量都具有静态存储期。

  • 线程存储期用于并发程序设计,程序执行可被分为多个线程。具有线程存储期的对象,从被声明时到线程结束一直存在。以关键字 _Thread_local 声明一个对象时,每个线程都获得该变量的私有备份。

  • 块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存;当退出这个块时,释放刚才为变量分配的内存

    变长数组稍有不同,它们的存储期从声明处到块的末尾,而不是从块的开始处到块的末尾。

  • malloc ()等内存分配函数分配的对象具有动态分配存储期,这样的对象需要使用free ()函数进行销毁 (后面介绍)

存储类别说明符

存储类型说明符 含义
auto 自动存储期
register 寄存器存储类型
static 静态存储期
extern 表示声明的变量定义在别处
_Thread_local 线程存储期
typedef 与内存存储无关,只是语法的原因

储存类别

存储类别 存储期 作用域 链接 声明方式
自动 自动 块内
寄存器 自动 块内,使用关键字:register
静态外部链接 静态 文件 外部 所有函数外
静态内部链接 静态 文件 内部 所有函数外,使用关键字:static
静态无链接 静态 块内,使用关键字:static

自动变量

  • 属于自动存储类别的变量具有自动存储期、块作用域且无链接。

  • 默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。如果为了清楚地表达这是一个自动存储类别的变量,可以显式使用关键字auto,如下所示:

    int main()
    {
        auto int a;
        ...
    }
    

    注意:关键字 auto是存储类别说明符(siorage-class specifier)。auto关键字在C++中的用法完全不同,如果编写C/C++兼容的程序,最好不要使用auto作为存储类别说明符

  • 块作用域和无链接意味着只有在变量定义所在的块中才能通过变量名访问该变量(当然,参数用于传递变量的值和地址给另一个函数,但是这是间接的方法)。另一个函数可以使用同名变量,但是该变量是储存在不同内存位置上的另一个变量。

  • 当内层块中声明的变量与外层块中的变量同名时,内层块会隐藏外层块的定义,但是离开内层块后,外层块变量的作用域又回到了原来的作用域。如下演示代码

    #define _CRT_SECURE_NO_WARNINGS
    #include <stdio.h>
    
    int main()
    {
        int a = 100;
        printf("循环开始前,外层定义的a = %d,&a = %p\n\n", a, &a);
        
        for (int i = 0; i < 5; i++)
        {
            int a = 1;
            a += i;
            printf("第 %d 次循环,内层定义的a = %d,&a = %p\n", i + 1, a, &a);
        }
    
        printf("\n循环结束后,外层定义的a = %d,&a = %p\n", a, &a);
        return 0;
    }
    
    // 运行结果
    /*
    循环开始前,外层定义的a = 100,&a = 010FFDFC
    
    第 1 次循环,内层定义的a = 1,&a = 010FFDE4
    第 2 次循环,内层定义的a = 2,&a = 010FFDE4
    第 3 次循环,内层定义的a = 3,&a = 010FFDE4
    第 4 次循环,内层定义的a = 4,&a = 010FFDE4
    第 5 次循环,内层定义的a = 5,&a = 010FFDE4
    
    循环结束后,外层定义的a = 100,&a = 010FFDFC
    */
    
  • 自动变量的初始化:自动变量不会初始化,除非显式初始化它。考虑下面的声明:

    int main(void)
    {
        int repid;
        int tents =5;
        ...
    }
    /* tents 变量被初始化为 5,但是 repid 变量的值是之前占用分配给 repid 的空间中的任意值(如果有的话),别指望这个值是0。*/
    

寄存器变量

  • 变量通常储存在计算机内存中。如果幸运的话,寄存器变量储存在CPU的寄存器中,或者概括地说,储存在最快的可用内存中。

  • 与普通变量相比,访问和处理这些变量的速度更快。

  • 由于寄存器变量储存在寄存器而非内存中,所以无法获取寄存器变量的地址

  • 绝大多数方面,寄存器变量和自动变量都一样。也就是说,它们都是块作用域、无链接和自动存储期。使用存储类别说明符register 便可声明寄存器变量:

    int mian()
    {
        register int quick;
        ...
    }
    
  • 刚才说“如果幸运的话”,是因为声明变量为register 类别与直接命令相比更像是一种请求。编译器必须根据寄存器或最快可用内存的数量衡量你的请求,或者直接忽略你的请求,所以可能不会如你所愿。在这种情况下,寄存器变量就变成普通的自动变量。即使是这样,仍然不能对该变量使用地址运算符。

  • 在函数头中使用关键字register,便可请求形参是寄存器变量

    void macho(register int n);
    
  • 可声明为register的数据类型有限。例如,处理器中的寄存器可能没有足够大的空间来储存 double类型的值。

块作用域的静态变量

  • 静态变量(siatic variable)听起来自相矛盾,像是一个不可变的变量。实际上,静态的意思是该变量在内存中原地不动,并不是说它的值不变。也可以称为局部静态变量

  • 前面提到过,可以创建具有静态存储期、块作用域的局部变量。这些变量和自动变量一样,具有相同的作用域,但是程序离开它们所在的函数后,这些变量不会消失。也就是说,这种变量具有块作用域、无链接,但是具有静态存储期。如下例子。

    #include <stdio.h>
    
    int more()
    {
        static int count = 0;
        count++;
        return count;
    }
    
    int main()
    {
        for (int i = 0; i < 5; i++)
        {
            printf("%d\n", more());
        }
        return 0;
    }
    
    // 运行结果
    /*
    1
    2
    3
    4
    5
    */
    

    上例中,count变量只在编译more函数时初始化一次。如果逐步调试该程序会发现,程序跳过了 static int count = 0; 这条声明,这是因为静态变量和外部变量在程序被载入时已经执行完毕,把这条声明放在 more 函数中是为了告诉编译器只有 more 函数才能看到该变量

  • 如果未显示初始化静态变量,他们会被默认初始化为0

    static int count;	// count会被初始化为0
    
  • 不能在函数的形参中使用static关键字

    void func(static int a);	// 不允许
    

外部链接的静态变量

  • 外部链接的静态变量具有文件作用域、外部链接和静态存储期。该类别有时称为外部存储类别(external storage class),属于该类别的变量称为外部变量(external variable)。

  • 把变量的定义性声明(defining declaration)放在在所有函数的外面便创建了外部变量。

    #include <stdio.h>
    
    int a;				// 外部定义的变量
    double arr[10];		// 外部定义的数组
    
    int main()
    {
        ...
    }
    
  • 为了指出该函数使用了外部变量,可以在函数中用关键字 extern再次声明。

    #include <stdio.h>
    
    int a;
    double arr[10];
    
    int main()
    {
        extern int a;		// 表明该变量是外部定义的变量。不声明也可以,直接使用
        extern double arr[];// 表明该数组是外部定义的数组。不声明也可以,直接使用
        ...
    }
    

    注意,在main()中声明 arr 数组时(这是可选的声明)不用指明数组大小,因为第1次声明已经提供了数组大小信息。main()中的两条extern声明完全可以省略,因为外部变量具有文件作用域,所以 a 和 arr 从声明处到文件结尾都可见。它们出现在那里,仅为了说明 main()函数要使用这两个变量。

  • 如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须用extern在该文件中声明该变量。

    // 假设这是 01.c 文件中的内容
    int a = 12;
    
    // 假设这是 main.c 文件中的内容
    #include <stdio.h>
    
    extern int a;	// 想要使用 01.c 文件的外部变量:a,必须使用extern声明该变量
    
    int main()
    {
        printf("%d", a);
        return 0;
    }
    

    注意:这两个文件要在同一工程下。最简单的方法就是先创建vs的空项目,再在源文件中添加两个文件。不要使用#include引用,让这两段代码处于不同翻译单元中

初始化外部变量

外部变量和自动变量类似,也可以被显式初始化。与自动变量不同的是,如果未初始化外部变量,它们会被自动初始化为0。这一原则也适用于外部定义的数组元素

与自动变量的情况不同,只能使用常量表达式初始化文件作用域变量:

#include <stdio.h>

int x = 10;				// 可以,10是常量
int y = 3 + 20;			// 可以,常量表达式
size_t z = sizeof(int);	// 可以,sizeof 表达式被视为常量表达式
int a = 2 * x;			// 不可以,x 是变量

int main()
{
    ...
}

定义和声明

#include <stdio.h>

int tern = 1;	// tern被定义

int main()
{
    extern int tern;	// 使用在别处定义的tern
    
    ...
}

这里,tern被声明了两次。第1次声明为变量预留了存储空间,该声明构成了变量的定义。第2次声明只告诉编译器使用之前已创建的tern 变量,所以这不是定义。

第1次声明被称为定义式声明(defining declaration),第2次声明被称为引用式声明(referencing declaration)。关键字extern表明该声明不是定义,因为它指示编译器去别处查询其定义。

假设这样写

#include <stdio.h>

extern int tern;

int main()
{
    ...
}

编译器会假设tern实际的定义在该程序的别处,也许在别的文件中。该声明并不会引起分配存储空间。因此,不要用关键字extern创建外部定义,只用它来引用现有的外部定义。

内部链接的静态变量

该存储类别的变量具有静态存储期、文件作用域和内部链接。在所有函数外部(这点与外部变量相同),用存储类别说明符static定义的变量具有这种存储类别:

#include <stdio.h>

static int svil = 1;	// 静态变量,内部链接

int main()
{
    ...
}

普通的外部变量可用于同一程序中任意文件中的函数,但是内部链接的静态变量只能用于同一翻译单元中的函数。

可以使用存储类别说明符extern,在函数中重复声明任何具有文件作用域的变量。这样的声明并不会改变其链接属性。考虑下面的代码:

static int stayhome = 1;

int main()
{
    extern int stayhome;	// 使用定义在别处的stayhome
    ...
}

extern int stayhome; 该声明指明了main() 函数中使用的stayhom变量定义在别处,但是这并没有改变stayhom的内部链接属性

多文件

只有当程序由多个翻译单元组成时,才体现区别内部链接和外部链接的重要性。

  • 复杂的C程序通常由多个单独的源代码文件组成。
  • 有时,这些文件可能要共享一个外部变量。C通过在一个文件中进行定义式声明,然后在其他文件中进行引用式声明来实现共享。
  • 也就是说,除了一个定义式声明外,其他声明都要使用extern 关键字。而且,只有定义式声明才能初始化变量。
  • 注意,如果外部变量定义在一个文件中,那么其他文件在使用该变量之前必须先声明它(用extern关键字)。也就是说,在某文件中对外部变量进行定义式声明只是单方面允许其他文件使用该变量,其他文件在用extern 声明之前不能直接使用它。

存储类别和函数

函数也有存储类别,可以是外部函数(默认)和静态函数。C99新增了第3种类别:内联函数,后面介绍

  • 外部函数可以被其他文件的函数访问

  • 静态函数只能用于其定义所在的文件

  • 除非使用关键字 static,否则一般函数声明默认为 extern

double gamma(double);			// 该函数默认为外部函数
static double beaa(int, int);
extern double delta(double, int);

在同一程序中,其他翻译单元可以调用gamma()和delta(),但是不能调用beta(),因为static存储类别说明符创建的函数属于私有的,这样做避免了名称冲突

通常的做法是:用extern 关键字定义其他文件中的函数,这样做是为了表明当前文件中使用的函数定义在别处。如果不使用任何关键字,函数声明默认为extern

内存分配

所有程序都必须预留足够的内存来存储程序使用的数据,这些内存是自动分配的。例如如下声明:

float x;		// 为一个float类型的值预留了足够的空间
char place[] = "Dancing Oxen Creek";	// 可以根据显示指定分配一定数量的内存
int plates[100];	// 预留了100个内存位置,每个位置都用于存储int类型的值

声明还为内存提供了一个标识符。因此,可以使用x或place识别数据。静态数据在程序载入内存时分配,而自动数据在程序执行块时分配,并在程序离开该块时销毁。

malloc()函数

定义在stdlib.h头文件中

C能做的不止这些。可以在程序运行时分配更多的内存。

  • 主要的工具是malloc()函数,该函数接受一个参数:所需的内存字节数。

  • malloc()函数会找到合适的空闲内存块,这样的内存是匿名的。也就是说,ma1loc()分配内存,但是不会为其赋名。

  • 但是会返回动态分配内存块的首字节地址。因此,可以把该地址赋给一个指针变量,并使用指针访问这块内存。因为char 表示1字节,malloc()的返回类型通常被定义为指向char 的指针。

    然而,从ANSIC标准开始,C使用一个新的类型:指向 void 的指针。该类型相当于一个“通用指针”。malloc()函数可用于返回指向数组的指针、指向结构的指针等,所以通常该函数的返回值会被强制转换为匹配的类型。在ANSIC中,应该坚持使用强制类型转换,提高代码的可读性。然而,把指向 void 的指针赋给任意类型的指针完全不用考虑类型匹配的问题。

  • 如果 ma11oc()分配内存失败,将返回空指针。

double * ptd;
ptd = (double*)malloc(30 * sizeof(double));

上面代码表示请求30个double类型大小的内存空间,并设置指针ptd指向该位置

注意:指针ptd被声明为指向一个double类型,而不是含有30个double类型值的数组。所以ptd指向这30个double类型的数组的首地址,那么就可以想使用数组名一样使用他。也就是ptd[0]表示该内存区域的首地址,ptd[1]表示访问第2个元素

free()函数

定义在stdlib.h头文件中

通常,malloc 和free函数要配套使用

  • free()函数的参数是之前ma1loc()返回的地址,该函数释放之前 ma11oc()分配的内存。因此,动态分配内存的存储期从调用malloc()分配内存到调用free()释放内存为止。
  • mal1oc()和free()管理着一个内存池。每次调用malloc()分配内存给程序使用,每次调用 free()把内存归还内存池中,这样便可重复使用这些内存。

如果内存分配失败,可以调用exit()函数结束程序,其原型在 stdlib.h中。

EXIT_FAILURE的值也被定义在 stdlib.h中。标准提供了两个返回值以保证在所有操作系统中都能正常工作:EXIT_SUCCESS(或者,相当于0)表示普通的程序结束,EXIT_FAILURE 表示程序异常中止。一些操作系统(包括UNIX、Linux和 Windows)还接受一些表示其他运行错误的整数值。

//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//
// Exit and Abort
//
//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// Argument values for exit()
#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1

stdlib.h文件中关于EXIT_SUCCESS和EXIT_FAILURE的定义

malloc和free函数使用示例如下

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

int main()
{
    // 获取数组大小
    int arr_len;
    printf("输入想要创建的数组大小:");
    scanf("%d", &arr_len);
    
    // 创建int指针,并指向malloc返回的地址
    int* p = (int*)malloc(arr_len * sizeof(int));
    
    // 如果p为空指针,说明内存分配失败,退出程序
    if (p == NULL)
    {
        printf("动态内存创建失败,退出程序\n");
        exit(0);
    }

    // 为创建出来的数组赋初始值
    for (int i = 0; i < arr_len; i++)
    {
        p[i] = i;
    }

    // 打印数组中元素的值和地址
    for (int i = 0; i < arr_len; i++)
    {
        printf("值:%d;地址:%p\n", p[i], &p[i]);
    }

    // 释放malloc分配的动态内存,接收的参数是malloc返回的地址
    free(p);

    return 0;
}

一定要注意用free释放malloc分配的内存

静态内存的数量在编译时是固定的,在程序运行期间也不会改变。自动变量使用的内存数量在程序执行期间自动增加或减少。但是动态分配的内存数量只会增加,除非用free()进行释放。否则可能导致内存耗尽。这类问题被称为内存泄漏(memory leak)

calloc()函数

  • 使用方法和 malloc()类似,在 ANSI之前,calloc()也返回指向 char 的指针;在 ANSI之后,返回指向 void的指针。如果要储存不同的类型,应使用强制类型转换运算符。
  • calloc()函数接受两个无符号整数作为参数(ANS1规定是sizet类型)。第1个参数是所需的存储单元数量,第2个参数是存储单元的大小(以字节为单位)。
  • calloc()函数还有一个特性:他把块中的所有位都设置为0(注意,在某些硬件系统中,不是把所有位都设置为0来表示浮点值0)。
  • free()函数也可用于释放 calloc()分配的内存。

使用示例

long *newmem;
newmem = (long*)calloc(100, sizeof(long));	// 表示创建100个long类型的空间,并将首地址赋值给newmem

类型限定符

我们通常用类型和存储类别来描述一个变量。

  • C90新增了两个属性:恒常性(consancy)和易变性(volaniliy)。这两个属性可以分别用关键字 const 和 volatile 来声明,以这两个关键字创建的类型是限定类型(qualifed tpe)。

  • C99标准新增了第3个限定符:restrict,用于提高编译器优化。

  • C11标准新增了第4个限定符: _Atomic。C11提供一个可选库,由stdatomic.h管理,以支持并发程序设计,而目Atomic是可选支持项。

  • C99为类型限定符增加了一个新属性:它们现在是幂等的(idempotenr)!这个属性听起来很强大,其实意思是可以在一条声明中多次使用同一个限定符,多余的限定符将被忽略:

    const const const int n = 6;	// 与 const int n = 6; 是等价的
    

const 类型限定符

  • 被const修饰的变量变为只读的

    const int change;
    change = 12;	// 报错,不允许修改
    
  • 可以初始化const变量

    const int change = 12;		// 可以初始化,初始化后也不可以修改他的值
    
  • 可以用const创建不可修改的数组

    const int arr[2] = {1,2};	// 数组内的值不可修改
    

在指针中使用const

  • 常量指针,指针的指向可以修改,但是指向的值不可修改

    int a = 1;
    int b  = 10;
    
    // 常量指针,const放在 * 之前就行。与 int const * p; 是等价的
    const int *p = &a;
    p = 2;	// 错误,指针指向的值不可修改
    p = &b;	// 可以,指针的指向可以修改
    
  • 指针常量,指针的指向不可以修噶,指针指向的值可以修改

    int a = 1;
    int b  = 10;
    
    // 指针常量,const放在 * 之后
    int * const p = &a;
    p = 2;	// 可以,指针指向的值可以修改
    p = &b;	// 错误,指针的指向不可以修改
    
  • const修饰常量和修饰指针,指针指向不可修改,指针指向的值不可修改

    int a = 1;
    int b  = 10;
    
    // 两个const,分别修饰指针和常量。与 int const * const p; 等价
    const int * const p = &a;
    p = 2;	// 错误,指针指向的值不可以修改
    p = &b;	// 错误,指针的指向不可以修改
    

形参声明中使用const

const 关键字的常见用法是声明为函数形参的指针。

因为数组传递给函数时,只能传递地址,所以为了保证数组内的数据不被修改,需要在函数的形参中使用const修饰

#include <stdio.h>

int sum(const int arr[], int arr_len)	// 形参中使用const修饰
{
    ...
}

int main()
{
    int arr[3] = {1,2,3};
    sum(arr, 3);
    ...
}

对全局数据使用const

使用全局数据是一种冒险的方法,因为这样做暴露的数据,程序的任何部分都可以修改数据,如果把数据设置为const,就可以避免这种危险

可创建 const变量、const数组、const结构体

#include <stdio.h>

const int a = 1;
const int arr[3] = {1,2,3};
const struct book{
  	int count;
    float price;
    char book_name[10];
};

int main()
{
    ...
}

volatile类型限定符

智能的(进行优化的)编译器会注意到多次使用的变量,但并未改变它的值。于是编译器把该变量的值临时储存在寄存器中,然后在程序的其他地方需要使用该变量时,从寄存器中(而不是从原始内存位置上)读取该变量的值,以节约时间。这个过程被称为高速缓存(caching)。

通常,高速缓存是个不错的优化方案,但是如果一些其他语句在下面两条语句之间改变了x的值,就不能这样优化了。

val1 = x;

/* 一些其他语句,如果改变了x的值,但是编译器使用高速缓冲,x的值被放在寄存器中,不知道x的值被改变,还是从高速缓存中取值。导致我明明在代码中修改了x的值,并赋值给val2,但是val2的值还是等于x修改之前的值
解决方法就是在定义x变量是用volatile修饰,编译器就不会使用高速缓冲,而是老老实实的从原始内存的位置上取值 */
...		// 其他语句
    
val2 = x;

如果声明中没有volatile关键字,编译器会假定变量的值在使用过程中不变,然后再尝试优化代码。

可以同时使用const和volatile限定一个值:volatile const int a;

restrict类型限定符

restrict关键字只能用于指针,表明该指针是访问数据对象唯一且初始的方式

使用restrict关键字可以让编译器优化某部分代码以更好的计算,如下

int ar[10];
// 使用restrict修饰的指针restar是访问malloc()所分配内存的唯一方式
int * restrict restar = (int*)malloc(10 * sizeof(int));
// 指针par不是访问数组ar的唯一方式,所以不用restrict关键字
int * par = ar;

for(int i = 0;i < 10;i++)
{
    par[i] += 5;
    restar[i] += 5;
    ar[n] *= 2;
    par[i] +=3;
    restar[i] += 3;
}

由于之前声明了 restar 是访问它所指向的数据块的唯一且初始的方式,编译器可以把涉及restar的两条语句替换成下面这条语句进行优化计算,效果相同:

restar[n]+=8;/*可以进行替换*/

但是,如果把与par相关的两条语句替换成下面的语句,将导致计算错误:

par[n]+=8;/*给出错误的结果*/

这是因为par不是指向数组的唯一方式,for循环在par两次访问相同的数据之间,用ar 改变了该数据的值。

restrict关键字有两个读者。一个是编译器,该关键字告知编译器可以自由假定一些优化方案。另一个读者是用户,该关键字告知用户要使用满足restrict要求的参数。总而言之,编译器不会检查用户是否遵循这一限制,但是无视它后果自负。

_Atomic类型限定符

并发程序设计把程序执行分成可以同时执行的多个线程。这给程序设计带来了新的挑战,包括如何管理访问相同数据的不同线程。

C11通过包含可选的头文件stdatomic.h和threads.h,提供了一些可选的(不是必须实现的)管理方法。值得注意的是,要通过各种宏函数来访问原子类型。

当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象。例如,下面的代码:

int hogs;//普通声明
hogs =12;//普通赋值

可以替换成:

Atomic int hogs;//hogs 是一个原子类型的变量
atomic store(&hogs,12);//stdatomic.h中的宏

这里,在hogs中储存12是一个原子过程,其他线程不能访问hogs。编写这种代码的前提是,编译器要支持这一新特性。

旧关键字的新用法

C99允许把类型限定符和储存类别说明符:static 放在函数原型和函数的形参的初始方括号中。

  • 对于类型限定符而已,这样做是为现有功能提供一个可替代的语法。如下

    void ofmouth(int * const a1, int * restrict a2, int n);
    // 该声明表示a1是一个int类型的指针常量,还表明a2是一个restrict指针
    

    可替换为:

    void ofmouth(int a1[const], int a2[restrict], int n);
    // 与上面的声明完全等价,注意:需要编译器支持
    
  • 新标准为static引入了一种与以前用法不相关的新用法。现在static除了表明静态存储类别变量的作用域和链接外,新的用法还告诉编译器如何使用形式参数。如下

    double statick(double ar[static 20]);
    // static的这种用法表明,函数调用中的实际参数应该是一个指向数组首元素的指针,且该数组至少有20个元素
    

    这种用法的目的是让编译器使用这些信息优化函数的编码

posted @ 2024-06-28 12:21  7七柒  阅读(16)  评论(0编辑  收藏  举报