顾名思义,要解释好这个问题,需要理解好两部分内容:const关键字和指针。
const关键字基本知识
首先,解释一下什么是const关键字。const关键字是单词constant的缩写,代表常量。const这个关键字用来修饰一个具有显式类型
的变量,表示其修饰的变量值不可更改,否则会报错。
那么,什么叫显示类型呢?
在Visual Studio 2022下输入以下代码:
int main() { const var; //编译器报错:缺少显式类型(假定int) }
这一点还是很好理解的,const目的是告诉编译器,后面的这个变量是一个常量,而C++作为“强类型语言”,任何一个“量”都要具备一个基本的数据类型(int,
double这些),这里所说的“缺少显式类型”就可以理解为缺少基本的数据类型。所以,以上代码应当修改为如下形式。
int main() { const int var; //这里只是为了说明问题,没有进行初始化,实际操作必须在定义时就进行初始化 //写成int
const var; 也可以,但是不推荐 }
这样的定义实际上就是在告诉编译器,变量var首先具有一个基本类型int,其次还是不可修改的常量,理解了这一点后,就可以解释带const关键字的指针了。注释掉的写法首先告诉编译器,变量var首先是一个常量,随后,编译器希望知道这个常量是什么数据类型(即怎样解析这个变量中的数据),然后编译器找到int,问题解决。一般而言,最好采用前一种写法(个人认为原因可能是醒目一点ㄟ(≧◇≦)ㄏ)。这里还有一个问题需要注意:
常量必须在定义时就进行初始化,这里只是为了说明问题而没有在代码中进行初始化,实际编译时要注意,后文中未初始化的部分如果未加说明也是如此。
指针部分的基本知识
然后,再来粗浅地解释指针部分。指针类型是一个相当特殊的类型,其本质仍然是数据
,这个数据表示内存中某一个区域的位置。在指针所指示的内存区域中,存放着具有一个基本类型的数据。对于一个指针而言,它需要完成两个基本任务:
第一,告诉编译器数据的位置;第二,告诉编译器在需要提取这个位置的数据时,该采用什么样的读取方式
(比如数据是整型,就用整型的存取方式),这也就是构成指针的两个要素。最终,在定义指针类型变量时,写成以下形式。
int main() { int* p; //内存区域中数据的存取方式为int,p是一个存放指针数据的变量 }
不论数据采取何种存取方式(int也好,double也罢),任何一个指针类型的变量存储的都是一个固定长度的整型数值(32位情况下占用4个字节,64位情况下占用8个字节),这个数值表示数据的存储位置,通俗来说就是地址编号。最后,形成了一个叫做“int*”的类型。这里要注意,
“int*”是一个新的数据类型,int仅仅代表的是指针所指向内容存取方式而已,其它的像“float*”“double*”仅仅代表了存取方式不同。在定义中,
“*”会和变量名先绑定,告诉编译器这是一个指针,然后再与具体的存取方式结合起来形成一个完整的指针类型
。究其本质,它们都是一个形如“0x00AB”的整型的地址编号而已,长度都是固定的。体现这一点最好的例子莫过于void类型的指针了,void指针将变量的存取问题暂时搁置,先解决较重要的地址问题。对于void类型指针,不能够进行解析处理,否则会报错,见下例。
#include<iostream>//C++ //#include<stdio.h> //C语言 int main() { int a = 0;
void* p = &a;//p中存放了整型数a的地址,但没有告诉编译器该采用何种存取方式
/*以下两种写法都会报错,原因都是编译器不知道该采用何种方法提取p地址中的数据*/ //cout << *p << endl; //C++,报错
//printf("%d\n", *p);//C语言,报错 /*正确写法是利用强制类型转换告诉编译器存取方式*/ cout << *(int*)p <<
endl; //C++ printf("%d\n", *(int*)p); //C语言 }
带const的指针分析
铺垫了这么多,下面就可以解释带const关键字的指针问题了,从最简单的情况入手,直接给出以下代码,然后做详细分析。
int main() { const int* var; //int const* var; 也可以,但不推荐 }
我们已经知道“const 数据类型 变量名”可以定义一个值不可修改的常变量,“数据类型* 变量名”可以定义一个指针类型的变量。这里要注意之前提到过的问题:
const必须结合一个基本数据类型,然后才能形成一个基本数据类型的常量类型。
在此处,“*”先与var结合,告诉编译器变量var是一个指针,要获取真正的数据,需要先通过地址找到其位置。编译器知道这一点后还需要确认这个真正数据的存取方式,于是int结合const告诉编译器
指针var背后真正的数据是一个不可修改的整型值。
这里还有一点要注意:在上述定义中,变量var可以不进行初始化,原因在于var本身并没有被const修饰(被修饰的是它指向的数据)。
换换顺序,写成这样该如何理解呢?
int main() { int* const var; }
到这里,你可能会问了,const必须要结合一个基本数据类型,究竟哪一个才是基本数据类型呢?实际上,这也是我在一开始给出"int
const var"这种定义方式的意图所在。在这种情况下,var先结合const告诉编译器,var这个变量是不可修改的。然后编译器就会
继续确认这个不可修改的变量究竟是什么样的基本数据类型
,于是,“*”继续结合变量名var,这下编译器知道了,var是一个指针类型,这个指针不能改动。确认过这一点后,编译器希望知道这个特殊的、不能改动的指针应该用何种方式解析(至少也得告诉它暂时不知道——void,在这里已经确认了是int),往前一看,这个变量还被int修饰,最后编译器就得到了关于变量var的一切信息了。
最后,综合一下,给出以下进阶形式的代码,基本分析思路已经注释完备,理解起来应该没有问题。
int main() { //const var; 告诉编译器,定义一个变量var,这个变量值不可改动
/*-----有了const,编译器希望知道var究竟是什么类型-----*/ //* const var; 告诉编译器,这个常量基本类型是指针类型
/*-----编译器希望知道这个指针怎样解析来获取实际数据-----*/ //const int* const
var;告诉编译器,以常量整型的解析方法来解析实际数据 /*-----最终形式-----*/ const int* const var;
/*-----也可以写成这样,但不推荐-----*/ //int const* const var; }
有了以上分析,就不难作出如下总结。
对第一种定义方式而言,var这个指针保存的地址是可变的,而背后的事迹数据,由于是const int类型,不能够修改。
int main() { int test_1 = 10; int test_2 = 20; const int* var =
&test_1;//存储的地址可变,地址背后代表的实际值不可变 var = &test_2; //正确,此时指针指向test_2 //*var =
test_1; //错误 }
对第二种定义方式而言,var这个指针本身被const修饰,保存的地址是不可变的,但这个地址背后的实际数据可以修改。
int main() { int test_1 = 10; int test_2 = 20; int* const var = &test_1; //var
= &test_2; //错误,var中的地址是const类型,无法修改 *var = test_2; //正确,var指向的实际数据可以改动 }
对于第三种定义方式,var这个指针本身被const修饰,保存的地址不可变。同时,地址指向的实际数据也被const修饰,同样无法修改。
int main() { int test_1 = 10; int test_2 = 20; const int* const var = &test_1;
//var = &test_2; //错误 //*var = test_2; //错误 }
写在最后
1. 在int main(){}函数体中,如果缺少return语句,编译器会自动填充。
If we don't place an explicit return statement at the end of main(), a return
0; statement is inserted automatically. In the program examples in this book, I
do not place an explicitreturn statement.
— Essential C++ by Stanley B. Lippman
2. 对于指针以及修饰符优先级的理解是关键,尤其是指针。个人认为理解好这一点后,理解C++中的引用也会轻松许多。
3. 受限于笔者初学C/C++ ,水平有限,不妥之处万望各位有心人不吝指正。