顾名思义,要解释好这个问题,需要理解好两部分内容: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//C++
//#include //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关键字的指针问题了,从最简单的情况入手,直接给出以下代码,然后做详细分析。
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 explicit return statement.
— Essential C++ by Stanley B. Lippman
2. 对于指针以及修饰符优先级的理解是关键,尤其是指针。个人认为理解好这一点后,理解C++中的引用也会轻松许多。
3. 受限于笔者初学C/C++ ,水平有限,不妥之处万望各位有心人不吝指正。