C/C++中的const--常量指针与指针常量

问题解析


常量指针


常量指针是指向常量的指针,指针指向的内存地址的内容是不可修改的。
指针指向了一个常量,但是指针本身是一个变量

定义const int *p=&a;

这条语句告诉编译器,*p是常量,不能将*p作为左值进行操作。但这里的指针p还是一个变量,它的内容存放常量的地址,所以先声明常量指针再初始化是允许的,指针也是允许修改的

示例

int a = 0,b = 1;
const int *p;    //  声明常量指针p
p=&a;            //  p指向a
p=&b;            //  修改指针的值p让其指向b,允许
*p=2;            //  修改指针所指向的变量的值,不允许

指针常量


指针常量是指针的常量,它是不可改变地址的指针,但可以对它所指向的内容进行修改。
指针本身是一个常量,但是指着指向一个变量

指针常量定义int * const p=&a;

告诉编译器,p是常量,不能作为左值进行操作,但允许修改其指向的内容,即*p是可修改的。指针常量必须在声明的同时对其初始化,不允许先声明一个指针常量随后再对其赋值,这和声明一般的常量是一样的

示例

int a = 0,b = 1;
int *const p1 = &a; 
int *const p2;           //  不允许,必须常量定义时必须对其初始化
p2 = &b;                 //  不允许,p2是指向a的常量, 不允许作为左值
*p1 = 2;                 //  允许,  指针指向的是一个变量, 可以修改指针*p1的值

指向常量的指针常量


如其名,不仅仅指针本身是一个常量(不可以修改其指向,指针指向的也是一个常量)
定义为const int * const p = &ca;

总结


依据右左法则右左法则:首先从最里面未定义的标识符看起,然后先往右看,再往左看。每当遇到圆括号时,就应该掉转阅读方向。一旦解析完圆括号里面所有的东西,就跳出圆括号。重复这个过程直到整个声明解析完毕。(右左法则不是C标准里面的内容,它是从C标准的声明规定中归纳出来的方法。C标准的声明规则,是用来解决如何创建声明的,而右左法则是用来解决如何辩识一个声明的)
当然我们也可以依据编译原理的思想,来分析const修饰的语义

指针常量

定义分析
char * const cp指针常量
右左法则cp is a const pointer to character
语义are neat

常量指针

定义分析
const char * p;常量指针
char const * pC++标准规定,const关键字放在类型或变量名之前等价的,C++里面没有const*的运算符
右左法则p is a pointer to const character;
语义const修饰的char *, 说明该指针指向的变量是一个常量

const进阶


指针常量和常量指针我们已经明白是怎么回事了,当然我们今天说的不仅仅是这些,因为我们还有一个神乎其神的东西:编译器。语言也好,编译器也罢,语言的特性,往往需要强大编译器的支持,而编译器往往会在我们的代码中进行进一步的优化,前面提到的RVO-编译器返回值优化 就是这么回事,那么我们通过一个示例来看看,指针常量与常量指针更奇特的地方。

代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//  依据右左法则
//
//  char * const cp; ( * 读成 pointer to )
//  cp is a const pointer to character
//  此时, const修饰指针变量cp,说明该指针是一个常量
//
//  const char * p;
//  p is a pointer to const character;
//  此时, const修饰的char *, 说明该指针指向的变量是一个常量
//
//  char const * p;
//  同上因为C++里面没有const*的运算符,所以const只能属于前面的类型。
//  C++标准规定,const关键字放在类型或变量名之前等价的。

// main function
int main(void)
{
    char *buf = "hello world";
    printf("栈区域地址%p  常量区%p\n\n", &buf, buf);

        //  字符串数组
    const char const str1[] = "abcd";
    const char const str2[] = "abcd";
    printf("[%p == %p] %d\n", &str1, &str2, &str1 == &str2);  //  栈区
    printf("[%p == %p] %d\n\n", str1, str2, str1 == str2);    // 栈区
    //strcpy(str1, "hello");
    printf("%s\n", str1);


    //  指向常量的指针, 指针本身的地址&p在栈中, 指向的地址p在常量区...
    const char * pstr1 = "abcd";
    const char * pstr2 = "abcd";
    printf("[%p == %p] %d\n", &pstr1, &pstr2, &pstr1 == &pstr2);    //  指针本身的地址在栈区
    printf("[%p == %p] %d\n\n", pstr1, pstr2, pstr1 == pstr2);      //  指针指向的地址在常量区
    //  指向常量的指针, 指针本身的指向可以修改
    pstr1 = "1234";
    pstr2 = "1234";
    printf("[%p == %p] %d\n", &pstr1, &pstr2, &pstr1 == &pstr2);    //
    printf("[%p == %p] %d\n\n", pstr1, pstr2, pstr1 == pstr2);

    pstr1 = "abcd";
    pstr2 = "1234";
    printf("[%p == %p] %d\n", &pstr1, &pstr2, &pstr1 == &pstr2);
    printf("[%p == %p] %d\n\n", pstr1, pstr2, pstr1 == pstr2);

    // 指针常量
    char * const pstr3 = "abcd";
    char * const pstr4 = "abcd";
    //pstr4 = "bcedf";          //  error, 此时指针的指向无法修改
    printf("[%p == %p] %d\n", &pstr3, &pstr4, &pstr3 == &pstr4);    // 指针作为局部常量存储在栈中 
    printf("[%p == %p] %d\n\n", pstr3, pstr4, pstr3 == pstr4);      // 字符串存储在常量区


    return EXIT_SUCCESS;
}

运行结果


tcc的运行结果
这里写图片描述
gcc的运行结果
这里写图片描述
clang的运行结果
这里写图片描述

我们拿了大神法布里斯·贝拉(FabriceBellard)的tcc与现代优化技术下的编译器的编译结果进行对比,我们可以很明显的发现。
大家都有一个共通点,均把字符串本身存储在了常量区,而把局部的变量和局部的常量存储在了栈中,这点我们已经在C程序的内存布局(Memory Layout) 中已经有讨论不是我们今天讨论的重点。

我们所关注的是,一个不同支之处在于在现代优化技术下的编译器,相同的字符串变量在内存中只存储了一份, 字符数组除外,因为字符数组本身是一段连续存储的字符变量。

疑问


但是我之前上面的代码其实有点疑问,那就是char * const pstr = "abcd" 时候,我们的常理会认为pstr是一个指针常量,那么我么就应该不能修改其指向,但是可以修改其指向的字符串的值,但是结果真的如此么
我们从上面一个示例程序可以很明显的发现,pstr3和pstr4作为指针常量,指针本身的读写(主要是写)限制,是由编译器处理的,因此存在了栈区,但是字符串本身”abcd”却放在了代码段(或者说常量区)。那么我们可以大胆假设
1 指针常量本身的指向无法修改,这个由编译器在编译阶段进行检查
2 指针所指向的字符串也是无法修改的,因为字符串常量存储在代码段(或者常量区,因为有些分类中是不存在常量区的,常量区与代码段共同组成代码区)中,这段区域是由操作系统管理的只读段

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


int main(void)
{
    //  The pointer itself is a constant,
    //  Point to cannot be modified,
    //  But point to a string can be modified
    char * const pstr = "abcd";  //  Pointer to a constant

    // I find that pstr(the address of "abcd") is in ReadOnly data
    // &pstr(the address of pstr) is in stack segment
    printf("%p  %p\n", pstr, &pstr);

    *(pstr + 2) = 'e';  //  segmentation fault (core dumped)
    printf("%c\n", *(pstr + 2));

    return EXIT_SUCCESS;
}

于是关于这个问题,我在stackoverflow上寻找到了解答segmentfault when modify char * const pstr = “abcd”;
回答的大神们都很好的回答了这个问题,把大家的答案总计一下大致说了这样一个意思
char * const pstr = “abcd” 这条语句是在定义常量时同时进行了初始化,那么初始化的变量就作为一个文本常量存储在了常量区。

我们来类比如下一条语句const int a = 10; 我们难道能够绕过a来修改变量本身的值10么,显然答案是否定的,当然和这条语句对比并不是很贴切,那么我们来看下面一条

对于 char *str = "abcd"; 很明显指针str在栈中存储,那么字符串”abcd”本身仍然放在了常量区,那么我们依照常量,是可以修改变量的值的,但是结果依旧是否定的,这个参见why do i get a segmentation fault when writing to a string initialized with char

因此请看下面代码

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


int main(void)
{


    char str1[] = "abcd";
    *(str1 + 2) = 'a';   // OK
    printf("%s\n", str1);

    char *pstr1 = str1;
    *(pstr1 + 2) = 'b'; // OK
    printf("%s\n", pstr1);

    char * const cpstr1 = str1;
    *(cpstr1 + 2) = 'c';    // OK
    printf("%s\n", cpstr1);

    char *pstr2 = "abcd";
    *(pstr2 + 2) = 'd';           //  segment fault
    printf("%s", pstr2);


    char * const cpstr2 = "abcd";  //  Pointer to a constant
    *(cpstr2 + 2) = 'e';  //  segmentation fault (core dumped)
    printf("%s\n", cpstr2);

    return EXIT_SUCCESS;
}

因此下面的代码无论const存在与否,都是在修改常量区的代码

char * /* const */ ptr = "abcd";
*(ptr+2) = 'e';

必然导致访存错误,引起segmentfault
如果你非要这样做的话请使用

char ptr[] = "abcd";
*(ptr+2) = 'e';

或者更复杂点

char str[] = "abcd";
char * /* const */ cpstr = str;
*(cpstr + 2) = 'a';    // OK
printf("%s\n", cpstr);
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值