• C Primer Plus(6) 中文版 第12章 存储类别、链接和内存管理 12.5 ANSI C类型限定符 12.6 关键概念 12.7 本章小结


    12.5 ANSI C类型限定符
    通常用通用类型和存储类别来描述一个变量。C90还新增了两个属性:恒常性(constancy)和易变性(volatility)。这两个属性可以分别用关键字const和volatile来声明,以这两个关键字创建的类型是限定类型(qualified type)。C99标准新增了第3个限定符:restrict,用于提高编译器优化。C11标准新增了第4个限定符:_Atomic。C11提供了一个可选库,由stdatomic.h管理,以支持并发程序设计,而且_Atomic是可选支持项。
    C99为类型限定符增加了一个新属性:它们现在是幂等的(idempotent)!这个属性听起来很强大,其实意思是可以在一条声明中多次使用同一个限定符,多余的限定符将被忽略:
    const const const int n = 6; //与const int n = 6; 相同
    有了这个新属性,就可以编写类似下面的代码:
    typedef const int zip;
    const zip q = 8;
    12.5.1 const类型限定符
    以const关键字声明的对象,其值不能通过赋值或递增、递减来修改。
    但是,可以初始化const变量。该声明让变量成为只读变量。初始化后,就不能再改变它的值。 
    可以用const关键字创建不允许修改的数组。
    1.在指针和形参声明中使用const
    指针则复杂一些,因为要区分是限定指针本身为const还是限定指针指向的值为const。下面的声明:
    const float *pf; /*pf指向一个float类型的const值*/
    创建的pf指向不能被修改的值,而pf本身的值可以改变。可以设置该指针指向其他const值。 
    float *const pf; /*pt是一个const指针*/
    创建的指针pt本身的值不能更改。pt必须指向同一个地址,但是它所指向的值可以改变。
    const float *const pf;
    表明pf既不能指向别处,它所指向的值也不能改变。
    还可以把const放在第3个位置:
    float const *pf; //与const float *pf;相同
    把const放在类型名之后、*之前,说明该指针不能用于改变它所指向的值。简而言之,const放在*左侧任何位置,限定了指针指向的数据不能改变;const放在*的右侧,限定了指针本身改变。
    const关键字的常见用法是声明为函数形参的指针。const保证了指针形式的参数指向的数据不被修改。
    在函数原型和函数头,形参声明const int array[]与const int *array相同,所以该声明表明不能更改array指向的数据。
    ANSI C库遵循这种做法。如果一个指针仅用于给函数访问值,应将其声明为一个指向const限定类型的指针。如果要用指针更改主调函数中的数据,就不使用const关键字。
    2.对全局数据使用const
    使用全局变量是一种冒险的方法,因为这样做暴露了数据。程序的任何部分都能更改数据。如果把数据设置为const,就可避免这样的危险,因此用const限定符声明全局数据很合理。可以创建const变量、const数组和const结构(结构是一种复合结构)。
    然而,在文件间共享const数据要小心。可以采用两个策略。第一,遵循外部变量的常用规则,即在一个文件中使用定义式声明,在其他文件中使用引用式声明(用extern关键字):
    /*file1.c -- 定义了一些外部const变量*/
    const double PI = 3.14159;
    const char *MONTHS[12] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October",
                            "November", "December"};
    /*file2.c -- 使用定义在别处的外部const变量*/
    extern const double PI;
    extern const char *MONTHS[];
    另一种方案是,把const变量放在一个头文件中,然后在其他文件中包含该头文件:
    /*constant.h -- 定义了一些外部const变量*/
    static const double PI = 3.14159;
    static const char *MONTHS[12] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", 
    "October", "November", "December"}; 
    /*file1.c -- 定义了一些外部const变量*/
    #include "constant.h"
    /*file2.c -- 使用定义在别处的外部const变量*/
    #include "constant.h"
    这种方案必须在头文件中用关键字static声明全局const变量。如果去掉static,那么在file1.c和file2.c中包含constant.h将导致每个文件中都有一个相同标识符的定义式声明,C标准不允许这样做。实际上,这种方案相当于给每个文件提供了一个单独的数据副本(注意,以static声明的文件作用域变量具有内部链接属性)。由于每个副本只对该文件可见,所以无法用这些数据和其他文件通信。不过没关系,它们都是完全相同(每个文件都包含相同的文件)的const数据(声明时使用了const关键字),这不是问题。
    头文件方案的好处是,方便你偷懒,不用惦记着在一个文件中使用定义式声明,在其他文件中使用引用式声明。所有的文件都只需包含同一个头文件即可。但它的缺点是,数据是重复的。对于前面的例子而言,这不算什么问题,但是如果const数据包含庞大的数据,就不能视而不见了。
    12.5.2 volatile类型限定符
    volatile限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值。通常,它被用于硬件地址以及在其他程序或同时运行的线程中共享数据。例如,一个地址上可能存储着当前的时钟时间,无论程序做什么,地址上的值都随时间的变化而改变。或者一个地址用于接受另一台计算机传入的信息。
    volatile的语法和const一样:
    volatile int locl; /*locl是一个易变的位置*/
    volatile int *ploc; /*ploc是一个指向易变的位置的指针*/
    以上代码把locl声明为volatile变量,把ploc声明为指向volatile变量的指针。
    为何ANSI委员把volatile关键字放入标准?原因是它涉及编译器的优化。 
    val1 = x;
    /*一些不使用x的代码*/
    val2 = x;
    智能的(进行优化的)编译器会注意到以上代码使用了两次,但并未改变它的值。于是编译器把x的值临时存储在寄存器中,然后在val2需要使用x时,才从寄存器中(而不是从原始内存位置上)读取x的值,以节约时间,这个过程被称为高速缓存(caching)。通常,高速缓存是个不错的优化方案,但是如果一些其他代理在以上两条语句之间改变了x的值,就不能这样优化了。如果没有volatile关键字,编译器就不知道这种事情是否会发生。因此,为安全起见,编译器不会进行高速缓存。这是在ANSI之前的情况。现在,如果声明中没有volatile关键字,编译器会假定变量的值在使用过程中不变,然后再尝试优化代码。
    可以同时用const和volatile限定一个值。例如,通常用const把硬件始终设置为程序不能更改的变量,但是可以通过代理改变,这时用volatile。
    只能在声明中同时使用这两个限定符,它们的顺序不重要,如下所示:
    volatile const int loc;
    const volatile int *ploc;
    12.5.3 restrict类型限定符
    restrict关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始化的方式。要弄明白为什么这样做有用,先看几个例子。考虑下面的代码:
    int ar[10];
    int *restrict restar = (int *)malloc( 10 * sizeof(int) );
    int *par = ar;
    这里,指针restar是访问malloc()所分配内存的唯一且初始的方式。因此,可以用restrict关键字限定它。而指针par既不是访问ar数组中数据的初始方式,也不是唯一方式。所以不用把它设置为restrict。
    现在考虑下面稍复杂的例子,其中n是int类型:
    for( n = 0; n < 10; n++ ){
        par[n] += 5;
        restar[n] += 5;
        ar[n] *= 2;
        par[n] += 3;
        restar[n] += 3;

    由于之前声明了restar是访问它所指向的数据块的唯一且初始化的方式,编译器可以把涉及restar的两条语句替换成下面这条语句,效果相同。
    restar[n] += 8; /*可以进行替换*/
    但是,如果把与par相关的两条语句替换成下面的语句,将导致计算错误:
    par[n] += 8; /*给出错误的结果*/
    这是因为for循环在par两次访问相同的数据之间,用ar改变了该数据的值。
    在本例中,如果未使用restrict关键字,编译器就必须假设最坏的情况(即,在两次使用指针之间,其他的标识符可能已经改变了数据)。如果用了restrict关键字,编译器就可以选择捷径优化计算。
    restrict限定符还可用于函数形参中的指针。这意味着编译器可以假定在函数体内其他标识符不会修改该指针指向的数据,而且编译器可以尝试对其优化,使其不做别的用途。例如,C库有两个函数用于把一个位置上的字节拷贝到另一个位置。在C99中,这两个函数的原型是:
    void *memcpy( void *restrict s1, const void *restrict s2, size_t n );
    void *memmove( void *s1, const void *s2, size_t n );
    这两个函数都从位置s2把n字节拷贝到位置s1。memcpy()函数要求两个位置不重叠,但是memmove()没有这样的要求。声明s1和s2为restrict说明这两个指针都是访问相应数据的唯一方式,所以它们不能访问相同块的数据。这满足了memcpy()无重叠的要求。memmove()函数允许重叠,它在拷贝数据时不得不更小心,以防在使用数据之前就先覆盖了数据。
    restrict关键字有两个读者。一个是编译器,该关键字告知编译器可以自由假定一些优化方案。另一个读者是用户,该关键字告知用户要使用满足restrict要求的参数。总而言之,编译器不会检查用户是否遵循这一限制,但是无视它后果自负。
    12.5.4 _Atomic类型限定符(C11)
    并发程序设计把程序分成几个可以同时执行的多个线程。这给程序设计带来了新的挑战,包括如何管理访问相同数据的不同线程。C11通过包含可选的头文件stdtomic.h和threads.h,提供了一些可选的(不是必须实现的)管理方法。值得注意的是,要通过各种宏函数来访问原子类型。当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象。例如,下面的代码:
    int hogs; //普通声明
    hogs = 12; //普通赋值
    可以替换成:
    _Atomic int hogs;            //hogs是一个原子类型的变量 
    atomic_store( &hogs, 12 );  //stdatomic.h中的宏
    这里,在hogs中存储12是一个原子过程,其他线程不能访问hogs。
    能写这种代码的前提是,编译器要支持这一新特性。
    12.5.5 旧关键字的新位置
    C99允许把类型限定符和存储类别说明符static放在放在函数原型和函数头的形式参数的初始方括号中。对于类型限定符而言,这样做为现有功能提供了一个替代的语句。例如,下面是旧式语法的声明:
    void ofmouth( int *const a1, int *restrict a2, int n ); //以前的风格
    如上一节所述,新的等价语法如下:
    void ofmouth( int a1[const], int a2[restrict], int n ); //C99允许
    根据新标准,在声明函数形参时,指针表示法和数组表示法都可以使用这两个限定符。
    static的情况不同,因为新标准为static引入了一种与以前用法不相关的新用法。现在,static除了表示静态存储类别变量的作用域和链接
    外,新的用法告知编译器如何使用形式参数。例如,考虑下面的原型:
    double stick( double ar[static 20] );
    static的这种用法表明,函数调用中的实际参数应该是一个指向数组首元素的指针,且该数组至少有20个元素。这种用法的目的是让编译器使用这些信息优化函数的编码。为何给static新增一个完全不同的用法?C标准委员会不愿意创建新的关键字,因为这样会让以前用新关键字作为标识符的程序无效。
    12.6 关键概念
    可以把自动内存看作是可重复利用的工作区。 
    12.7 本章小结 

  • 相关阅读:
    MySQL在线升级方案
    服务器访问本机图片nginx配置
    林旅强 | AI+开源时代 - 开发者与治理者的机遇与挑战
    grpc学习golang版( 六、服务器流式传输 )
    Java并发-生产者消费者实现
    论文代码测试
    暗影骑士擎Pro 之 安装Ubuntu18.04 双系统 踩坑记录(一)
    基础复习——为activity补充活动信息——利用资源文件配置字符串——利用元数据传递配置信息——给页面注册快捷方式...
    【ShaderLab 碎片边境美式卡通角色_“ospreycaptain“_角色渲染(第三篇)】
    Python Django路由urls.py详解
  • 原文地址:https://blog.csdn.net/weixin_40186813/article/details/126393074