问题解析
常量指针
常量指针是指向常量的指针,指针指向的内存地址的内容是不可修改的。
即指针指向了一个常量,但是指针本身是一个变量
定义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 * p | C++标准规定,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);