这部分内容比较简单,我这里直接先做总结,然后通过写三个测试代码,体会其中的关键点
一、总结
1、const使得变量具有只读属性(但是不一定就是不能更改)
2、const不能定义真正意义上的常量(因为有的用const定义的变量,仍然可以更改)
3、const将具有全局生命期的变量存储于只读存储区(这个是对现代编译器是这样的,但是对ANSI编译器,仍然可以更改)
4、volatile强制编译器减少优化,必须每次从内存中取值
5、const修饰的变量不是一个真的常量,它只是告诉编译器该变量不能出现在赋值符号的左边
6、在现在C编译器中,修改const全局变量将导致程序崩溃
7、c语言中字符串字面量存储于只读存储区中,在程序中需要使用const char*指针(这句话的意思就是用const char*修饰的字符串字面量时(包括局部和全局的字符串字面量),字符串字面量是存储在全局只读存储区的,不能更改,更改会导致程序崩溃或者段错误)
8、const修饰函数参数表示在函数体内不希望改变参数的值(注意:这里是不希望,那到底能不能更改,这得分情况)
9、const修饰函数返回值表示返回值不可更改,多用于返回指针情况
二、下面通过几个测试代码体会上面结论(平台:Ubuntu10 gcc 编译器)
第1个例子是const修饰变量情况
#include <stdio.h>
const int g_cc = 4;
int main()
{
const const int cc = 0x01;
int* p = (int*)&cc;
printf("cc = %d *p = 0x%x
",cc,*p);
//cc = 2; //编译通过,运行错误,因为cc被定义成const局部变量,不能出现在赋值符号左边,运行时导致程序段错误
*p = 3; //编译和运行都通过,因为cc是局部变量,所以不管是ANSI还是现代GCC编译器都可以更改,同时也说明了用const修饰变量只是说明这个变量不能出现在赋值符号的左边,但是依然可以更改,但是假如如果cc是全局变量,那就不一定了,如果是ANSI编译器是可以更改的(你可以用BCC编译器试下,BCC就是早期的ANSI编译器)因为早期编译器把const修饰的变量还是存储在全局数据区可以更改,如果是现在的VC或者GCC编译器是不可以更改的,因为现代编译器把const修饰的全局变量存储在全局只读存储区中,更改会出错
printf("cc = %d *p = %d
",cc,*p);
p = (int*)&g_cc;
printf("g_cc = %d *p = %d
",g_cc,*p);
//*p = 5; //编译通过,运行错误,因为g_cc被定义成const全局变量,又因为GCC属于现代编译器所以g_cc被分配到全局只读存储区,不能更改,更改导致段错误
//printf("g_cc = %d *p = %d
",g_cc,*p);
return 0;
}
上面的代码你可以把屏蔽部分代码打开自己调试,其实是不能运行的,代码注释解释的很清楚,这里不说了,看下输出结果:
其实我开始调试时cc变量的类型是unsigend char,出现了一个意外问题,你们看下输出,然后自己想下为什么?(这其实是指针类型问题,后面我会讲这个问题)
第2个例子是const修饰函数返回值、函数参数、字符串情况
注意:我会在程序里面提问18个问题,你看看你们能不能回答出来答案
#include <stdio.h>
const unsigned char *s1 = "G_hello world";
unsigned char a = 9;
unsigned char* fun(const unsigned char s,const unsigned char *str)
{
unsigned char *p = (unsigned char*)&s;
//s = 2; //(1)为什么编译错误
*p = 1; //(2)p指针指向s,但是s是const修饰的,不能更改,但是这里为啥能够更改
printf("s = %d *p = %d
",s,*p);
p = (unsigned char*)str;
//*p = '_'; //(3)为什么编译通过,运行段错误
printf("str = %s
p = %s
",str,p);
return "ABCDEF GHIJK"; //(4)这种定义的字符串字面量和使用字符指针指向字符串字面量有啥区别,还是一样的?
}
int main()
{
const unsigned char *s2 = "Hello world";//(5)s2指针指向的内容能不能更改?
unsigned char *s3 = "LMNOP ORST"; //(6)你们看到s3比s2少了一个const,那编译会不会出错? s3指向内容能不能更改呢?那和s2有啥区别呢?
unsigned char *s4 = "LMNOP ORST"; //(7)s4和s3指向的内容都是一样的,那他们地址是不是一样的呢?
unsigned char s5[] = "LMNOP ORST"; //(8)s5和s3一个是数组,一个是指针,那他们有啥区别呢?而且他们内容也是相同的,那他们的地址是不是也是一样的呢
const unsigned char s6[] = "LMNOP ORST";//(9)s6比s5多了一个const,多了这个导致有啥区别么?
const unsigned char i = 2;
const static unsigned char j = 3; //(10)j比i多了一个static,多了这个导致有啥区别么?
unsigned char *pc = fun(i,s2);
printf("&i = %p &a = %p
",&i,&a);
printf("&j = %p j = %d
",&j,j);
printf("s1 = %p s2 = %p
",s1,s2);
printf("s3 = %p s4 = %p
",s3,s4);
printf("s5 = %p s5 = %s
",s5,s5);
printf("s6 = %p s6 = %s
",s6,s6);
printf("pc = %p
pc = %s
",pc,pc);
//(11)通过观测这么多变量,字符指针,数组你发现什么规律没(从地址去观察)
//j = 4; //编译出错,我们通过终端打印发现j是存储在全局只读区域中,所以不能更改
pc = &j; //编译出现警告,运行通过,因为指针可以指向任何地方
//*pc = 5; //编译通过,运行段错误,因为pc指向的是全局只读区域,所以不能更改
//*pc = '!'; //(12)编译通过,为什么运行段错误
//*s2 = '_'; //(13)为什么编译错误
pc = s2; //编译出现警告,因为类型不一样
//*pc = '$'; //(13)编译通过,为什么运行段错误
//*s3 = 'A'; //(14)编译通过,为什么运行段错误
pc = s3;
//*pc = '_'; //(15)编译通过,为什么运行段错误
printf("更改前:s5 = %s
",s5);
s5[0] = 'A';
pc = s5;
*(pc + 1) = 'B';
printf("更改后:pc = %s s5 = %s
",pc,s5);
printf("更改前:s6 = %s
",s6);
//s6[0] = 'A'; //(16)为什么不能更改
pc = &s6[0];
*pc = 'A'; //(17)为什么用一个指针却可以更改s6呢,再从地址观察s5和s6,有啥发现
printf("更改后:pc = %s s6 = %s
",pc,s6);
return 0;
}
我们看下终端输出:
现在回答上面的17个答案:
(1):因为被const关键字修饰变量,不能出现在赋值符号左边,所以编译出错
(2):因为用const定义变量只是告诉编译器不能出现赋值符号左边,但是本质还是变量,这里就是局部变量,还是可以通过指针修改它的值
(3):编译肯定通过,因为p是指针当然可以指向任何地方,运行错误是因为p指针指向的是字符串字面量,而字符串字面量是存储在全局只读存储区,所以运行错误(具体为什么是全局只读区域,后面我在(11)提问里面会说)
(4):其实是一样的,因为我们从终端地址发现他们都在0x80487XXH内存区域里面,而这个区域就是全局只读区域,都是不能更改的(具体为什么是全局只读区域,后面我在(11)提问里面会说)
(5):不能更改的,因为定义的字符串指针是指向字符串字面量,而字符串字面量存储的区域是全局只读区,所以不能更改,有的人问,你怎么知道是全局只读区域,这个在(11)的提问里面回答这个问题
(6):编译是不会出错的(包括编译和执行),s3指向的内容也是不能更改的,这个在后面我会给你验证的,其实你从终端打印的地址也能看出来的,因为你发现他们都是存储在0x80487XX的地址区域,而这个区域都是全局只读区域,所以不能更改,还有和s2有什么区别,其实我认为是没有区别的,因为他们都不能更改,而且存储的区域也都一样,所以我认为没有区别
(7):通过终端打印我们发现地址居然一样,编译器居然为了节省空间(我猜想的),只存储一个"LMNOP ORST",当然他们都是存在只读内存空间,不能更改,比较安全,如果是可更改空间,那可就出大事了,修改其中一个内容值,另外一个变量内容也跟着更改了
(8):首先s3是字符指针,指向内容是一个字符串字面量,而且s3指向内容的区域是全局只读区域,所以不能更改,而s5是数组是可以更改的,而且s5是局部的,也就是存储在栈中,临时分配的内存,函数执行完释放掉,同时通过终端打印我们也发现s3和s5内存地址也是完全不一样的,相差很多,因为一个是全局只读区域,另一个是局部内存区域(就是栈)
(9):s6和s5的区别是,s5可以直接更改,就是s[0] = 'A';,而s6是不能直接更改的,s6[0] = 'A'编译器在编译时就会报错,但是他们都是存储在局部内存区域(就是栈),这个区域是可以通过指针进行更改的,所以s6还是可以更改的,通过终端打印发现他们地址也很接近
(10):j和i的区别是,j存储在全局只读区域,不能更改,i是存储在栈中,是可以更改的,但是不能直接更改,必须通过指针进行更改,通过终端打印也发现,j的地址和s1、a的地址都很接近,所以j肯定是全局只读区域(为什么是只读区域,后面我会验证,因为经过验证它不能更改)
(11):总结:
首先我们肯定知道s1肯定是全局区域,又由于s1不能更改(这个我没写进程序里面,你们可以自己去验证下, 其实真的不能更改),所以s1存储在全局只读区域,又因为s1跟j、s2、s3、s4、pc,所以这些变量存储的内容都是存储在全局只读区域内,不能更改,但是你们发现没,a变量肯定也是全局变量,但是它确是可以更改的,所以a和s1地址肯定不一样,通过终端打印发现,他们确实不挨着,而且相差也不是很多,因为他们都在全局区域内
其次:通过这个例子我们知道用const unsigned char*定义的指针指向了字符串字面量是不能更改的,而且是存储在全局只读存储区的(这里记住,即使没有const也是全局只读区域,s3就是这样的),要是也想把局部变量也定义到全局只读存储区中,需要用const static关键字(比如这里的j变量就是),而且我们还发现,字符串指针如果只向内容是一样的,编译器居然为了省空间,地址居然是一样的
再次: 字符串指针和数组,是有区别的,他们只向的内容存储的区域不一样,字符串指针是全局只读区域,而数组是栈中,可以更改,虽然有的加了onst但是通过指针还是可以更改
(12):因为pc指针指向fun函数返回的内容是存储在全局只读区域,不能更改,所以运行错误
(13):因为const定义变量是不能出现在赋值符号左边,而且s2指针,指向的内容是字符串字面量,是存储在全局只读区域内,是不能更改的
(14):因为s3指针,指向的内容是字符串字面量,是存储在全局只读区域内,是不能更改的
(15):同上
(16):因为s6是const关键字定义的局部变量,是不能出现在赋值符号左边,但是可以更改,不能这样直接更改,需要用指针进行更改
(17):通过终端打印发现s5和s6地址很接近,因为他们都是局部变量,存储在栈中,但是因为s6是用const关键字定义变量,是不能出现在赋值符号左边的,但是又因为s6是存储在局部变量区域,所以可以通过指针进行更改
volatile影响编译器编译的结果,指volatile 变量是随时可能发生变化的,与volatile变量有关的运算,不要进行编译优化,以免出错,(VC++ 在产生release版可执行码时会进行编译优化,加volatile关键字的变量有关的运算,将不进行编译优化。)。
例如:
volatile int i=10;
int j = i;
...
int k = i;
volatile 告诉编译器i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的可执行码会重新从i的地址读取数据放在k中。而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,
它会自动把上次读的数据放在k中。而不是重新从i里面读。这样一来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问,不会出错。
一般用在1.并行设备的硬件寄存器,如状态寄存器,2.中断服务程序用于检测中断的变量,3.多线程被线程共享的变量。
本文主要参考了"狄泰软件C进阶视频教程”
原文:https://blog.csdn.net/liuchunjie11/article/details/80333224