找回密码
 立即注册
首页 业界区 业界 奶奶都能看懂的 C++ —— const 限定符与指针 ...

奶奶都能看懂的 C++ —— const 限定符与指针

捐催制 昨天 10:55
上一篇我们讲了指针,这一篇先从 const 讲起。
常量

嗯。const,顾名思义,就是不变。给任何数据类型加上 const,就指明了这个变量不会再变化。任何试图修改变量的操作都会报错,无法通过编译。比如:
  1. const int a = 10;
  2. a = 11; //Error!
复制代码
当然,常量也必须在定义时初始化。
常量自己不能变,但这不代表不能使用。它可以被用于初始化其它对象:
  1. int b = a * 2;
  2. // b = 20
复制代码
很简单的东西,不是吗?接下来让我们结合一下上一篇的引用和指针。
常量引用

我们可以使用 const 限定修饰一个引用。由于引用本身就不可以更改它绑定的对象,所以这里 const 只是阻止了对绑定对象的修改而已:
  1. const int a = 10;
  2. int b = 10;
  3. const int &c = a;
  4. const int &d = b;
  5. int &e = a; //Error
  6. a = 11;// Error
  7. b = 11;// OK!
  8. c = 11;// Error
  9. d = 11;// Error
复制代码
看看上面的代码,对 a,c,d 的修改都会产生编译错误。我们一个个分析:

  • a 是常量,但是 e 是个普通引用,非常量不能绑定到常量
  • 对 a 的修改是不可行的,因为它是个常量
  • 对 b 的修改是可行的,因为它是整型变量
  • 对 c、d 的修改不可行,因为它们是常量引用,不可修改
也就是说……
const 限定符应用在引用上时,只是让 C++ 认为引用指向的对象不可以被修改,而实际指向的对象到底是否为常量(是否可以修改)是没有影响的。
如果已经在对象上施加 const,那么指向它的引用也必须添加,来保持类型一致。可以这么理解:如果引用不加 const,那么 C++ 就认为引用指向的对象可以修改,这显然和对象的不可修改性不符,编译器不允许这样的事情发生。
试试这样想吧:你可以在可以随意使用的瓶子上贴上勿动的标签,但是不能给不能动的瓶子贴上可动的标签
还记得我们曾经把引用比作瓶子上贴的标签,那么这里的 const 限定符就好像在标签上加上一句:不能动!
特殊用法

在继续前进之前,我们来看点奇怪的常量引用。
  1. int i = 10;
  2. double s = 3.14;
  3. const int &a = 1;
  4. const int &b = i * 2;
  5. const int &c = s * 2;
复制代码
wow,这里的3、4、5行居然把表达式赋给引用,会报错的。
既然我都说了是奇怪的引用,当然不会编译错误啦。这其实是常量引用的特殊用法:如果一个引用添加了 const 限定,那么编译器允许使用任意表达式(包括字面值、算式、对象),并且能够自动转换。
你一定已经在学习指针前,了解过自动转换了。如果两个变量的类型不匹配,那么编译器会尝试自动转换。
也就是说,上方代码与下方等效:
  1. int i = 10;
  2. double s = 3.14;
  3. int tmp1 = 1;
  4. const int &a = tmp1;
  5. int tmp2 = i * 2;
  6. const int &b = tmp2;
  7. int tmp3 = s * 2; //自动类型转换
  8. const int &c = tmp3;
  9. // b = 20, c = 6
复制代码
但也别搞混了,这个只在引用带有 const 时生效,普通引用由于是可变的,所以只能绑定一个数据类型匹配的变量。
恭喜你,你已经掌握了引用中的 const,我们一鼓作气,继续看看指针中的 const。
const 与指针

添加 const 限定
  1. const int a = 10;
  2. int b = 10;
  3. const int *s = &a;
  4. const int *t = &b;
  5. int *s1 = &a; //Error
  6. int *t1 = &b;
复制代码
为什么 s1 的定义会报错,但是其它就不报错呢?
我们在常量引用中提到,const 只是告诉编译器,认为指向的对象不可变。举一反三,const 应用于指针,则表示认为指针所指的对象(那个地址对应的对象)不可变。这就解释了 3,4 行的定义。
而第五行的报错也和上文所述相似,你所指的对象不可变,又怎么能够让指针认为所指的对象可变呢?这是不符合常理的。
认为指向对象不可变,也就是解引用后获得的对象不能变化
  1. *s = 11; //Error
  2. *t = 11; //Error
复制代码
但是和引用不一样,指针是对象,自己是可变的。上面的 const 限定只是让指针认为自己指向的对象不可变,但指针本身指向哪个对象是可变的。
  1. s = &b; //OK
  2. t = &a; //OK
复制代码
上面的代码完全可以正常运行。
常量指针

那怎么让指针自己不可变呢?嗯,这里事情逐渐变得复杂了起来。
  1. const int a = 10;
  2. int b = 10;
  3. int *const s = &a; //Error
  4. int *const t = &b;
  5. const int *const s1 = &a;
  6. const int *const t1 = &b;
复制代码
Wait Wait Wait 这有点太复杂了,我们还是一行行看。
注意到了吗?我们使用了 *const,它表示定义一个常量指针。顾名思义,指针本身是常量,不能变(不能改变保存的位置,即不能修改它指向的对象是哪一个)。
现在来看代码:

  • 第三行,它定义了一个本身是常量的指针(而认为指向的对象是可变的),但是却绑定到了不可变常量 a,因此报错(上文已经强调过,不能认为不可变的东西可变)
  • 第四行,它定义了一个本身是常量的指针,绑定到了变量 b,没问题。
  • 第五行,它定义了一个本身是常量的指针,且认为指向对象不可变,绑定到了不可变常量,没问题。
  • 第六行,它定义了一个本身是常量的指针,且认为指向对象不可变,绑定到了变量,没问题
为了更加清晰说明什么叫本身不可变,什么叫认为指向对象不可变,再给出以下代码:
  1. const int c = 11;
  2. t = &c; //Error
  3. s1 = &c; //Error
  4. t1 = &c; //Error
  5. *t = 12;
  6. *s1 = 12; //Error
  7. *t1 = 12; //Error
复制代码
仔细想想为什么那些行会报错吧。

  • 本身不可变的指针,不可以重新指向其它位置。因此 2,3,4 行报错
  • 之前提到过,如果指针认为自己指向的对象不可变,那么它解引用后不可变,所以 6,7 行报错
好好思考一下,分清楚什么是本身不可变,什么是认为指向的对象不可变(解引用后不可变)。
Tips:还是再回忆下 const 修饰的真正含义吧。如果认为指向对象不可变,那么这和指向对象实际是否可变没有任何关系。
如果你理解了,真得好好夸夸自己,连这么复杂的东西都搞懂了!
顶层与底层

我们把本身不可变的 const,称作顶层 const;认为指向对象不可变的 const,称作底层 const。
从之前的讲解中,我们不难得到推论:

  • 引用本身不可变,所以只有认为不可变的底层 const 存在。
  • 对于指针,如果放在距离变量名远的地方,那么是底层;距离变量名近的地方,是顶层
  • 底层 const 只和指针、引用有关,而顶层 const 可以修饰大部分对象
很好。接下来我们就要涉及一些更加深入的话题了。
我们曾经在特殊用法那里提过一嘴自动转换。众所周知,在执行赋值操作时,可能会进行自动转换。而变量可以转换为常量,常量也可以转换为变量:
  1. int a = 10;
  2. const int b = a; //b 被顶层 const 修饰,它本身不可变
  3. int c = b;
复制代码
但对于指针和引用来说,事情就更加复杂了。
当你赋值,涉及指针、引用时,源和目标的顶层 const 可以不同,但顶层(决定本身是否可变)必须满足自动转换(不可变拷贝到可变)。注意上面代码第二行,和下面代码最后一行。
  1. const int d = 20;
  2. const int *const p1 = &b;
  3. const int *p2 = &d;
  4. p1 = p2;//Error
  5. p2 = p1;//现在,p1,p2 都指向 b。
复制代码
对于底层 const,这决定了源、目标认为其所指向的对象是否可变。在此过程中,源和目标的底层 const 可以不同,但是底层(指向对象是否可变)必须满足自动转换(可变拷贝到不可变)
  1. int e = 1;
  2. int *p3 = &d; //Error
  3. int *p4 = &e;
  4. p2 = p4; //为变量的指针增加不可变修饰
复制代码
但注意一下,上面只是赋值操作,如果是新创建指针,那么顶层 const 无所谓(正如之前所述):
  1. const int *const m = p1;
  2. const int *m1 = p1;
复制代码
我知道你确实有点晕了。
总结一下,修改指针操作时,看等号左侧是否有顶层 const 很重要,有顶层 const 就不能修改;而任何操作时,都有必要去检查下等号右侧的底层 const,如果有,那么左边也必须有,否则左侧随意。
试试这样想吧:const 就是一种修饰。指针是瓶子的标签,你可以让瓶子(对象)本身不可变(顶层 const 修饰),但这样你必须在标签(指针)上写上“别动瓶子”(底层 const 修饰)。如果你看到了“别动”的标签(底层 const 修饰的指针),想根据这个标签给瓶子再贴一个标签,或者把别的瓶子上的标签移过来(创建新指针/修改旧指针),那么另一个标签上也得写“别动”(底层 const 修饰)。
如果你的标签上没有“别动”(没有底层 const),说明瓶子本身一定是可以动的(没有顶层 const),所以新创建的标签写不写“别动”都无所谓(有没有底层 const 并没有关系)。
而如果一个标签是强力胶,撕不下来(指针有顶层 const 修饰),那么它就不能移动。但是你还是可以根据这个标签,移动其它可以移动的标签(将其它无顶层 const 修饰的指针,赋值为它),或者创建一个新的标签,是否为强力胶都可以(创建新的指针时,顶层 const 修饰并不重要)。
用比喻来说,顶层 const 决定了标签有没有强力胶;底层 const 决定我们是否认为瓶子能动。如果有强力胶,一个标签本身就不能移动了,但不影响其它标签。如果我们根据一个标签,不认为瓶子能动,那么也就没办法再贴上能动的标签了。
注意了,我这里一直强调根据某个标签,是因为这是在指针的语境下来说的,我们必须根据指针来进行寻找对象、赋值等操作,而不是直接操作对象。好好想想,上文所述“根据某个标签”,指的就是赋值等号右侧的内容。
你也可以配上下面的表格举例,一起理解(注意代码是无法运行的,这里只是为了看清楚而写出了每个类型,所以没有用赋值的等号):
<ul>当操作为:创建指针,并赋值时:<ul>
int *p

相关推荐

您需要登录后才可以回帖 登录 | 立即注册