C语言中的类型限定符.const限定符

 

目录

1.1const限定符

1.1.1const限定符修饰普通对象

1.1.2const限定符修饰数组元素

1.1.3const限定符修饰指针类型对象

1.1.4const限定符修饰函数形参类型为数组的对象

类型限定符的本质含义

小伙伴!加油哦!


C语言中的类型限定符(type qualifier)用于指明一个对象的访存属性。C11标准中一共含有4种类型限定符,分别是const、volatile、restrict以及_Atomic。除了_Atomic这一类型限定符比较特殊外,对于其余三个类型限定符,当一个对象为指针类型的对象时,类型限定符与*号之间的摆放位置不同,该限定符所修饰的类型也会有所不同。此外,这些类型限定符可叠加使用。

我们将在12.1节中详细描述除_Atomic以外的其他三种类型限定符的摆放位置与修饰类型之间的关系,后面几节将不再赘述。而类型限定符的摆放与对类型的修饰也是C语言中的难点之一,希望各位能仔细阅读12.1节中的内容。

1.1const限定符

const限定符用于修饰一个对象,表明该对象是一个常量(constant)。被const修饰的对象只能初始化一次,之后它的值就不能被修改。在很多嵌入式系统中,全局const对象可能会与代码一起被存入ROM存储介质中。

当const限定符用于修饰一个对象时,如果将它放置于紧挨着对象标识符的前面,那么表明该const修饰此对象本身,该对象的值就不能被修改。在这种情况下,const也可以放置在类型的前面。像下面两语句分别将对象a与对象b声明为常量对象。


int const a = 100;         // 这里声明常量对象a,它具有int类型
const float b = 10.5f;     // 这里声明常量对象b,它具有float类型


上述代码中,无论是对象a还是对象b,都不能对它们的值进行修改。倘若通过指针做间接引用进行修改,

那么行为是未定义的,比如以下代码片段:


int const a = 10;          // 定义了常量对象a
void *p = (void*)&a;       // 这里通过万用指针对象p指向对象a的地址
*(int*)p += 10;            // 然后通过指针p来修改对象a的值


我们尝试在某个函数中编写上述代码然后运行,在一般桌面操作系统中常量对象a的值往往会变为20,由于常量对象a的存储空间在栈空间,栈存储空间本身是一个可读可写的存储空间,因此在这种情况下即便C语言编译器会对常量对象在编程语言语法上进行保护,但也不能保证在运行时常量对象的值一定是不变的。不过我们仍然不应该通过这种方式去修改一个常量对象。C语言标准明确指出,通过指针解引用(dereference)的方式去修改一个常量对象,其行为是未定义的。所谓“解引用”是指通过对指针对象做间接操作以访问该指针所指对象的值。当然,在某些嵌入式系统中,如果对一个存入ROM的全局常量对象进行修改,那么在运行时该常量的值要么不变,即该操作被存储器控制器视作为一个无效操作;要么系统直接发生异常。

这里再谈谈const的位置放置问题。在大部分源代码中,我们会看到const修饰一个对象时,往往会放在类型的前面。但在后面与指针类型结合的时候我们会发现,将const紧挨着放置于它所修饰的对象标识符之前,更容易判断当前修饰的是哪个类型,或者说哪一层解引用被视作一个常量。

const可以用于修饰任一类型的对象,包括基本类型,枚举、结构体、联合体等用户自定义类型,以及各种指针类型和数组元素类型。这里需要注意是,由于数组对象本身其地址是固定不变的,数组对象仅仅表征了一段存储空间的首地址以及对其元素的访问模式,所以C语言中没有一种限定符能用于修饰数组对象本身,限定符只能用于修饰数组元素。

下面将分别介绍const限定符如何修饰普通标量对象、数组元素、指针类型的对象,以及修饰后的对象的访问状态。

1.1.1const限定符修饰普通对象

当const修饰一个普通对象时,该对象的值将不能被修改。当一个const修饰一个复合类型对象时(比如一个结构体对象),那么该结构体对象中的所有成员的值都不能被修改。

代码清单1.1将展示这些常量对象的声明与使用。

代码清单1.1 const修饰普通对象

#include <stdio.h>
int main(int argc, const char* argv[])
{
    // 声明了一个int类型的常量对象a,但没有对它初始化
    int const a;
    
    // 即便如此,我们也不能再对常量对象a进行赋值,因此以下这条表达式是错误的:
    a = 100;
    
    // 声明了一个常量枚举对象e,并用MY_ENUM2枚举值对它初始化
    enum { MY_ENUM1, MY_ENUM2 } const e = MY_ENUM2;
    
    // 定义了一个匿名结构体,并用它声明了一个常量结构体对象s,这里的const也能放在struct前面
    struct
    {
        int a;

        
        struct
        {
            float f;
            double d;
        }inner;
    } const s = { .a = 10, .inner = { 10.5f, -20.5 } };
    
    // 这里对结构体对象s中的任一成员,包括其内嵌类型的成员,都不能修改
    s.inner.d = 30;     // 这条语句是错误的
    
    printf("result = %.1f\n", e + s.a + s.inner.f - s.inner.d);
    struct Object
    {
        int a, b;
        const int c;    // 这里成员对象c是一个常量
    };
    struct Object obj = { 10, 20, 30 };
    obj.a += obj.b;     // 这条语句没有问题
    // 以下这条语句将会出现编译报错,
    // 因为Object结构体中的成员对象c是常量,所以它的值不能被修改
    obj.c += 10;
}

代码清单1.1简单地介绍了const修饰一个普通类型的对象的使用情景。这里很明确地描述了当const修饰一个复合类型时的特性。

下面我们将描述const修饰数组元素的情况。

1.1.2const限定符修饰数组元素

在C语言中,一个数组对象仅仅表征了对一个连续存储空间的引用,所以在C语言实现中,一个数组对象的地址与其起始元素的首地址都是同一个地址,并且一个数组对象本身不具有“值”这个概念。因此,任一限定符都不能用于修饰一个数组对象,而只能用于修饰数组对象的元素。

代码清单1.2展示了const修饰数组元素的方式以及效果。

代码清单1.2 const修饰数组对象元素

#include <stdio.h>
int main(int argc, const char* argv[])
{
    // 声明了一个带有5个const int类型元素的数组对象a
    // 这里的const修饰的是a[i],而不是a自身
    int const a[] = { 1, 2, 3, 4, 5 };
    
    // 以下这条语句将产生编译错误,
    // 因为数组对象a中的每个元素都是常量,不能被修改
    a[0]++;
    

    // 这里首先定义了一个匿名枚举,然后用该枚举类型声明了一个
    // 带有3个常量元素的数组对象e。e的每个元素类型为const枚举类型,
    // 这里的const也能放在enum之前
    enum { MY_ENUM1, MY_ENUM2, MY_ENUM3 }
    const e[] = { MY_ENUM1, MY_ENUM2, MY_ENUM3 };
    
    e[1] = MY_ENUM3;     // 这条语句也是无法通过编译的
    
    // 这里定义了一个UN联合体类型,并用该类型声明了一个带有2个常量元素的数组对象u。
    // u的每个元素的类型为const union UN,这里的const也能放在union之前。
    union UN { int a; float f; }
    const u[] = { {.a = 10}, {.f = 2.5f} };
    
    u[1].f = 0.0f;       // 这条语句也无法通过编译
    
    printf("The value is: %.1f\n", a[0] + e[1] + u[1].f);
}

 

通过代码清单1.2的例子我们可以看到,const修饰一个数组对象时,产生作用的是该数组中的每个元素。然而,对于像代码清单1.2中数组对象a的类型,它仍然被表达为const int[5],表示具有5个const int类型元素的数组。我们要查看上述数组对象类型的话其实非常方便,比如我们要查看数组对象a的类型,我们在声明数组对象a语句下面写一句:a++;。然后,编译器会提示编译出错信息——Cannot increment value oftype'const int[5]',这就说明数组对象a的类型为constint[5]了。

下面我们将描述const所使用的最复杂的情景——与指针类型混用。

1.1.3const限定符修饰指针类型对象

在C语言中,限定符修饰一个对象是一个非常神奇的设定,这种神奇不亚于指向数组的指针与指向函数的指针这种表达形式。我们之前已经描述了const修饰一个普通对象的例子,比如——const int a=10;表示将对象a指定为一个常量,对象a的类型为const int。而且这里const与int之间的位置可以互换,不影响语义。而当const要修饰一个指针类型的对象时,内容就丰富了。这里涉及一个问题,我们需要指定是将指针对象本身指定为常量还是将该指针对象做了间接操作之后的值作为常量,或是将两者同时作为常量。下面我们将分别列出这三种表达方式。


int * const p1;        // 指定指针对象p1为常量
int const * p2;        // 指定指针对象p2做间接操作后的值作为常量
int const * const p3;  // 指定指针对象p3为常量,并且对它做间接引用后的值也作为常量


我们先对比看一下上述代码片段中的p1对象。这里const修饰的是p1指针对象,说明p1指针对象自身是一个常量,这意味着p1一旦被声明,它的值就不允许被修改,比如:p1=NULL;这条语句就是错误的。也就是说它不能指向其他地址,但是对*p1可以做修改,比如:*p1=20;这完全可行。我们看到,修饰p1指针对象的const紧靠在p1对象标识符之前(即在p1的左侧位置),并且在*号之后(即在*的右侧位置),此时p1的类型为int*const。

p2指针对象不是一个常量,const修饰的是(*p2),说明对p2做间接操作后,其值为一个常量,不允许对它进行修改。也就是说,像p2=NULL;这条语句是完全有效的,而像*p2=10;这条语句则是非法的。p2对象的类型为const int*。

p3指针对象是一个常量,并且对它做间接操作后的值也是一个常量。这意味着,像p3=NULL;以及*p3=10;这些都是非法的。p3指针对象的类型为constint*const。

通过上述三个指针对象的不同例子,我们可以发现const限定符在修饰一个指针对象时所产生的丰富多样性。这里大家要记住的是,在声明一个指针对象时,当const限定符摆放在所有*号的后面、紧靠在对象标识符之前,那么该const限定符修饰的是指针对象本身,即指针对象不能指向其他任何对象,它的值不能被修改。当const摆放在最靠近对象标识符的*号之前时,const修饰的实际上是*连同其后面的对象标识符,就比如(*p)的值,也就是说对*p的值不能进行修改。

所以从const所修饰的对象来看,我们就可以看它后面的*号位置。如果它后面没有*号,那么修饰的就是对象本身,否则它修饰的就是所有在此对象标识符之前、const之后的*做间接操作之后的对象值。像constint**pp;这里pp与(*pp)都不是常量,只有(**pp)才是常量。

除了从const所修饰对象的标识符这个视角看之外,还能通过const修饰的类型来看。

比如:p1是int*const类型,我们看const前面的类型,这里是int*,所以很显然这里的int*类型的对象是常量,不可被修改,也就是p1对象自身,因为p1的类型去除限定符之后就是int*类型。所以从类型角度看的话就是要看const之前的类型。

然后我们看p2的类型,它是const int*,我们不妨把const与int交换一下位置,这里的交换是不会影响语义的,只要不涉及越过*号的交换。我们看到p2的类型可描述为int const*,我们看摆放在const前面的类型是int,所以这里对于p2对象而言,(*p2)是一个常量,它是const int类型。

这么一来,当我们要声明一个指针对象,它自身是一个常量还是说对它做间接操作之后的对象是一个常量就有两种方式去判别了。当然,一般我们用这种类型判别作为一种验证方式,我们在思考时还是首选上一段所描述的const限定哪个对象的方法,不过主要还在于自己怎么思考和理解,而不用去强求采用哪种方式。当我们确定了*相对于const的位置之后也就能确定当前的const修饰的是哪种类型——const修饰的是跟在它后面的连同*包含在一起的那个对象。像对于p1来说,const后面就是p1,所以此时const修饰的就是p1对象,而p1对象的类型就是int*const;对于p2,const后面跟的是*p2,所以const修饰的就是(*p2),而(*p2)的类型很显然就是const int;对于p3则有两个const,最前面的const后面跟着的是(*const p3),所以(*p3)就是常量类型,即const int;而后一个const紧贴着p3,说明它修饰的就是p3对象,所以p3自身就是一个常量,这么一来,p3的类型就是const int*const。这里我们可以发现,当对一个对象做一次间接操作之后,比如(*p3),其类型就看该*之前的部分,而*之后的所有限定符都可以直接无视,所以(*p3)的类型就是constint,*后面的const可以无视之。而p3类型则需要把所有的const限定符都带上。

另外,我们还能观察到这么一个用来判定当前对象的值是否可修改的规律:如果当前对象标识符的前面紧贴着const,那么该对象的值无法修改,如果前面紧贴着的是*,那么该指针对象的值则可修改。比如p1之前紧贴着const,所以p1的值无法被修改;而p2之前紧贴着的是*,所以p2可被修改,但是(*p2)前面就紧贴着const了,所以(*p2)的值就不能被修改了。当我们掌握了这些识别const如何修饰指针对象的技巧之后,我们可以来点更复杂的情况。请见代码清单1.3。

代码清单1.3 const修饰指针对象的综合情况

#include <stdio.h>
// 这里定义了一个dummy函数,稍后会用到
static void dummy(int a)
{
    printf("param a = %d\n", a);


int main(int argc, const char* argv[])
{
    // 声明一个常量对象a,并将它初始化为10
    const int a = 10;
    
    // 声明一个普通对象b,并将它初始化位20
    int b = 20;
    
    // 声明一个变量指针对象p,并用对象a的地址对它初始化
    int const *p = &a;              // p的类型为const int *
    
    *p = 20;                    // 这句非法!(*p)是一个常量,其值不能被修改
    p = &b;                         // 这句没问题
    
    // 声明了一个常量指针对象q,并用对象b的地址对其初始化
    int * const q = &b;             // q的类型为int * const
    
    q = NULL;                           // 这句非法!q是一个常量,其值不能被修改
    *q += 10;                           // 这句没问题
    
    // 这里声明了一个变量指针对象cpp,并用指针对象p的地址对它初始化,
    // 注意,这里的const修饰的是(**cpp),只有(**cpp)不能被修改。
    // 此外,这里(*cpp)的类型为const int *,(**cpp)的类型为const int
    int const **cpp = &p;           // cpp的类型为const int **
    *cpp = &a;                      // 这里通过间接操作,使得指针对象p又指向了a
    if(p == &a)
        puts("p points to a!");
    
    cpp = NULL;                         // 这句没有问题
    **cpp = 0;                          // 这句错误!因为(**cpp)是一个常量,其值不允许被修改
    
    // 这里声明了一个常量指针对象cqq,并用指针对象q的地址对它初始化。
    // cqq前的const修饰的是cqq,说明cqq自身是一个常量,
    // int*后面的const修饰的是(*cqq),说明(*cqq)也是一个常量。
    // 也就是说,这里(*cqq)的类型为int * const,(**cqq)的类型为int
    int* const * const cqq = &q;    // cqq的类型为int * const * const
    **cqq += 100;       // 这句没有问题,(**cqq)的类型是int,不是一个常量
    printf("b = %d\n", b);
    
    *cqq = NULL;        // 这句错误!(*cqq)的类型为int * const,是一个常量

    cqq = NULL;         // 这句错误!cqq的类型为int * const * const,是一个常量
    
    // 声明一个数组对象arr,它含有3个int类型的元素
    int arr[3] = { 1, 2, 3 };
    
    // 这里声明了一个常量指针对象pArray,指向数组对象arr的地址,
    // pArray的类型为int (* const)[3],const修饰的是pArray对象标识符,
    // 这也说明const修饰的类型为int(*)[3],即pArray自身是一个常量
    int (* const pArray)[3] = &arr;
    (*pArray)[1] = 0;   // OK,没有问题
    pArray = NULL;      // 这句话则是非法的,pArray是常量,其值不允许被修改
    printf("arr[1] = %d\n", arr[1]);
    
    // 这里声明了一个指向函数的指针常量对象pFunc。
    // 这里的const修饰的是pFunc标识符,说明pFunc自身是一个常量。
    // const所修饰的类型则是void (*)(int)
    void (* const pFunc)(int) = &dummy;
    pFunc(100);                 // 没问题
    pFunc = NULL;       // 语法错误!pFunc是常量,其值不允许被修改
}

代码清单1.3中也体现了如何去判定一个指针对象的常量情况。正如之前提到的,对于从const修饰哪个对象而言则是看const后面的*号情况。我们把const后面的*号连同对象标识符一起放进去看,比如像对象p,它的声明符中const后面有一个*号,那么这个const就修饰了整个(*p),说明(*p)是一个常量。而对于指针对象q,const后面就只有一个对象标识符q,说明const修饰的就是q本身,那么q就是一个常量,而(*q)则不是常况我们也无需慌张,我们可以从右往左看各个const修饰的对象是啥。当我们在看某个const修饰哪个对象时可把其余的const全都忽略。比如我们看代码清单1.3中的cqq这个比较复杂的指针对象,首先在cqq之前有一个const限定符,说明这个const修饰的就是cqq自身。而对于int*后面的那个const,我们从这个const位置起从左往右看,可以看到const后面跟着的是*const cqq。正如之前所说的,我们把*之后的const都忽略掉可得到:*cqq,说明这里*之前的const修饰的是(*cqq),这意味着(*cqq)的值不允许被修改。而对于(**cqq),由于最左边的*前面没有const修饰,所以它不是常量,(**cqq)的值可以被修改。

代码清单cons_03也描述了对于指向数组的指针对象以及指向函数的指针对象如何用const修饰。其实原理也一样,直接在指针对象标识符前添加const即可。

以上讲的是const如何修饰一个指针对象的问题,而对于整个C语言类型系统而言还有一个比较重要的问题是——如何匹配一个含有const修饰的对象类型。这里除了涉及C语言的类型系统之外,还涉及一个类型安全问题。比如说,我们声明了一个常量对象const int a=10;,如果用它赋值给另一个普通变量对象,这是完全没有问题的,比如:int b=a;。因为无论变量b如何修改其值,常量a的值是不会受到影响的。但如果用一般的指针对象(如int*p=&a;)去指向某个常量对象会发生什么呢?当我们对指针对象p采用间接操作符来修改值的时候,比如*p=20;此时对象a的值就被改变了,这与对象a作为常量这一属性是相违背的。所以在C语言中,对一个常量对象做取地址操作之后的指针类型是在const后面直接加*号。比如对于“const type obj;”,&obj的类型就是const type*。这么做可使得对该指针类型做间接操作之后仍然保持常量类型。所以像上面的const inta;&a的类型为const int*,当然表示为int const*则更容易做类型判定。而一个const int*类型的指针对象是不能赋值给一个int*类型的指针对象的,除非用投射操作做类型强制转换。我们在判定一个对象取其地址之后的类型的方法也很简单——直接将原本对象的标识符变为*号即可。比如,这里的a声明为const int a,那么取其地址之后,&a的类型则是const int*(把a变成了*)。而对于代码清单cons_03中的q,它声明为int*const q,那么取其地址之后,&q的类型为int*const*(把q变成了*)。

正由于存在类型安全问题,所以等号操作符的左边表达式的类型如何去匹配等号操作符右边表达式的类型有一定学问。正如上一段所述,等号操作符右边的constint*类型不能隐式转换为等号操作符左边的int*类型;然而,等号操作符右边如果是int*类型,那么可以隐式转换为const int*类型与等号操作符左边的表达式匹配。换句话说,低限定的类型可以隐式转为高限定类型,而高限定类型则不允许隐式转为低限定类型。除了这种情况——int**不能隐式转换为const int**类型。代码清单1.4给出了int**不能隐式转换为const int**的理由。

#include <stdio.h>
int main(int argc, const char* argv[])
{
    // 这里先声明一个常量对象a
    const int a = 10;
    
    // 这里声明一个普通指针对象p,初始化为空
    int *p = NULL;
    
    // 这里声明一个const int**的指针对象pp,指向p的首地址。
    // 这里用投射操作做类型强制转换就是因为,
    // int**不能隐式转换为const int **类型。
    const int **pp = (const int**)&p;
    
    // 这里大家注意,由于*pp的类型是const int*,
    // &a的类型也是const int*,所以两者完全兼容。
    // 这条语句执行之后,p也就被间接指向了常量对象a的地址
    *pp = &a;
    
    if(p == &a)
        puts("p points to a!");
    
    // 最后通过指针对象p来间接修改常量对象a的值
    *p = 10;
    
    printf("a = %d\n", a);
}

从代码清单1.4中可以看到,倘若int**能隐式转换为const int**,那么我们通过一个中间普通指针对象就能绕过原先常量对象的访问权限,从而间接修改常量对象值的情况。这里,最具破坏力的语句就是——*pp=&a;。由于a是一个常量对象,而(*pp)的类型为const int*,完全与&a的类型相同,所以这个赋值没有任何问题,但所呈现的问题则是这条间接操作赋值语句把常量对象a的地址传递给了普通指向对象p。随后,普通指针对象p可以通过间接操作即可随意修改常量对象a的值。

因此,在C语言中不允许直接将int**隐式转换为const int**,而只能将int**隐式转换为constint*const*类型。如果代码清单const_04中的指针对象pp的类型是const int*const*,那么当对pp做第一次间接操作时,(*pp)的类型为int const*const,其值不允许被修改,从而保证了无法通过(*pp)将原先初始化时指向的指针对象间接地将它修改为指向某个常量对象。

以上已经基本把const限定符如何修饰一个对象以及修饰后所产生的效果都详细描述了。各位在看完这些内容后务必要反复实践,这样才能加深对限定符的了解,这块内容也确实不容易掌握。

1.1.4const限定符修饰函数形参类型为数组的对象

 

下面再谈一下const限定符如何修饰函数形参为数组类型的场合。一个函数的形参可以被表达为一个数组类型,但它本质上仍然是一个指针,既然是一个指针,那么它跟原生的数组对象就会有所不同。前面讲了,原生的数组对象本身是不可被修改的,因此没有所谓的用限定符修饰数组对象的这个概念,然而对于指针则不同,指针对象的值是可被修改的。如果我们要对以数组类型呈现的函数形参对象施加const限定,使得该形参值无法被修改,那么我们只需要将const限定符放置在[]下标操作符里面即可。如果[]中含有数值字面量或其他标识符,那么const放在它们的前面,即左侧位置。代码清单1.5展示了const修饰函数形参为数组类型的例子。

代码清单1.5

const修饰函数形参为数组类型的例子

​
#include <stdio.h>
// 这里,形参a相当于int * const类型
// 形参b相当于const int *类型
// 形参c相当于int const * const类型
static void Fun(int a[const 5], const int b[3],
                const int c[static const 4])
{
    a[0]++;      // OK!没有问题
    a = NULL;    // 错误!a是一个常量指针对象
    
    b[0]++;      // 错误!b[0]是const int类型,一个常量
    
    c[0]++;      // 错误!c[0]是const int类型,是一个常量
    c = NULL;    // 错误!c本身是一个常量,不能被修改
    
    printf("The sum is: %d\n", a[0] + b[1] + c[2]);
    
    b = NULL;    // OK!没有问题
}
int main(int argc, const char* argv[])
{
    int a[] = { 1, 2, 3, 4, 5 };
    int b[] = { 7, 8, 9 };
    int c[] = { 10, 11, 12, 13 };
    
    Fun(a, b, c);
}

​

代码清单1.5中给出了3种不同类型的形参,我们看每个形参的本质类型时也非常简单,直接将[]去掉,把里面的static等存储类说明符也全都去掉,只留限定符,然后把标识符变为*号即可。另外,果我们对使用数组下标操作符的对象类型是否为一个常量看不清,可以把数组下标形式变为间接操作形式,比如a[0]++;可以转换为(*(a+0))++;,语义是相同的,这样我们就能看到a[0]的类型其实与(*a)一样,都是int类型,而这里const修饰的是a自身。

类型限定符的本质含义

最后,我们来描述一下类型限定符的本质含义。所谓类型限定符很显然,它主要是限定类型的,尽管在文法上它也可以看作修饰一个对象,而像我们上面那种const限定的判定主要是以对象作为参照的,这里我们将再详细介绍如何根据类型进行判定。类型限定符所限定的是类型说明符(比如基本类型,枚举、结构体、联合体等用户自定义类型,还有稍后会讲的原子类型说明
),以及与类型说明符相结合的指针类型。我们假定有某个含有N级指针的类型Type(Type自身或许也包含着const限定符),那么Type const或者const Type都表示类型Type受到const限定。然而在C语言中,我们只能通过typedef将整个类型组合为一个类型标识符Type,这一点会在下一章描述。而像const int*这个类型,int*没有被视为一个整体,这里的const限定的仅仅是int类型,而不是整个int*类型。所以,在C语言中使用限定符后置法,将int*const这种写法看作为const限定int*类型。因此我们碰到除_Atomic之外的类型限定符,都看整个类型声明的最右边的限定符,最右边的限定符限定了在它左边的所有类型。如果对当前对象做了N次间接操作,那么我们就从右往左跳过N个*号,看类型限定符限定的情况。

像代码清单1.5中的Fun函数形参对象a,已知其类型为int*const,那么对于a自身来说,其类型最右边有常量;而对于(*a)或a[0]而言,我们用了一次间接操作符,那么我们从右往左看跳过一个*号,前面有没有const,说明(*a)的类型就是int,不是一个常量。同样,我们再分析一下形参对象c,它的类型可以通过上述方式解析出来,是一个int const*const类型。那么对于c而言,它前面就有一个const,所以c是一个常量;而对于(*c)或c[0]而言,做了一次间接操作之后看第一个*前面是否跟着一个const,我们看到确实有一个const,所以(*c)也是一个常量,其类型为const int。

我们可以再引申,观察代码清单1.3中的cpp和cqq。cpp指针对象的类型为int const**,那么对于cpp而言,类型最右边是一个*号而没有碰到const,所以cpp本身不是一个常量;而(*cpp)做了一次间接操作之后,跳过最右边的*号,其类型为int const*。很显然,这个类型的最右边也是一个*号而不存在const,所以(*cpp)也不是一个常量;最后再看(**cpp),做了第二次间接引用之后,其类型为int const,这里很明显就有一个const,所以(**cpp)是一个常量。而cqq也同样分析,cqq的类型为int*const*const,它前面就有一个const,所以cqq自身是一个常量;而做了第一次间接操作之后,(*cqq)的类型为int*const,很显然最右边是一个const,所以(*cqq)也是一个常量;最后看做第二次间接操作之后,(**cqq)的类型,它是int,没有const修饰,所以(**cqq)不是一个常量。

volatile、restrict以及_Atomic的类型限定符我下期发部哦!

小伙伴!加油哦!不要放弃,我陪你一起....................

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不,我只会粘贴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值