上一篇我们讲了指针,这一篇先从 const 讲起。

常量

嗯。const,顾名思义,就是不变。给任何数据类型加上 const,就指明了这个变量不会再变化。任何试图修改变量的操作都会报错,无法通过编译。比如:

const int a = 10;
a = 11; //Error!

当然,常量也必须在定义时初始化。

常量自己不能变,但这不代表不能使用。它可以被用于初始化其它对象:

int b = a * 2;
// b = 20

很简单的东西,不是吗?接下来让我们结合一下上一篇的引用和指针。

常量引用

我们可以使用 const 限定修饰一个引用。由于引用本身就不可以更改它绑定的对象,所以这里 const 只是阻止了对绑定对象的修改而已:

const int a = 10;
int b = 10;
const int &c = a;
const int &d = b;
int &e = a; //Error
a = 11;// Error
b = 11;// OK!
c = 11;// Error
d = 11;// Error

看看上面的代码,对 a,c,d 的修改都会产生编译错误。我们一个个分析:

  • a 是常量,但是 e 是个普通引用,非常量不能绑定到常量
  • a 的修改是不可行的,因为它是个常量
  • b 的修改是可行的,因为它是整型变量
  • cd 的修改不可行,因为它们是常量引用,不可修改

也就是说……

const 限定符应用在引用上时,只是让 C++ 认为引用指向的对象不可以被修改,而实际指向的对象到底是否为常量(是否可以修改)是没有影响的。

如果已经在对象上施加 const,那么指向它的引用也必须添加,来保持类型一致。可以这么理解:如果引用不加 const,那么 C++ 就认为引用指向的对象可以修改,这显然和对象的不可修改性不符,编译器不允许这样的事情发生。

试试这样想吧:你可以在可以随意使用的瓶子上贴上勿动的标签,但是不能给不能动的瓶子贴上可动的标签

还记得我们曾经把引用比作瓶子上贴的标签,那么这里的 const 限定符就好像在标签上加上一句:不能动!

特殊用法

在继续前进之前,我们来看点奇怪的常量引用。

int i = 10;
double s = 3.14;
const int &a = 1;
const int &b = i * 2;
const int &c = s * 2;

wow,这里的3、4、5行居然把表达式赋给引用,会报错的。

既然我都说了是奇怪的引用,当然不会编译错误啦。这其实是常量引用的特殊用法:如果一个引用添加了 const 限定,那么编译器允许使用任意表达式(包括字面值、算式、对象),并且能够自动转换。

你一定已经在学习指针前,了解过自动转换了。如果两个变量的类型不匹配,那么编译器会尝试自动转换。

也就是说,上方代码与下方等效:

int i = 10;
double s = 3.14;
int tmp1 = 1;
const int &a = tmp1;
int tmp2 = i * 2;
const int &b = tmp2;
int tmp3 = s * 2; //自动类型转换
const int &c = tmp3;
// b = 20, c = 6

但也别搞混了,这个只在引用带有 const 时生效,普通引用由于是可变的,所以只能绑定一个数据类型匹配的变量。

恭喜你,你已经掌握了引用中的 const,我们一鼓作气,继续看看指针中的 const

const 与指针

添加 const 限定

const int a = 10;
int b = 10;
const int *s = &a;
const int *t = &b;
int *s1 = &a; //Error
int *t1 = &b;

为什么 s1 的定义会报错,但是其它就不报错呢?

我们在常量引用中提到,const 只是告诉编译器,认为指向的对象不可变。举一反三,const 应用于指针,则表示认为指针所指的对象(那个地址对应的对象)不可变。这就解释了 3,4 行的定义。

而第五行的报错也和上文所述相似,你所指的对象不可变,又怎么能够让指针认为所指的对象可变呢?这是不符合常理的。

认为指向对象不可变,也就是解引用后获得的对象不能变化

*s = 11; //Error
*t = 11; //Error

但是和引用不一样,指针是对象,自己是可变的。上面的 const 限定只是让指针认为自己指向的对象不可变,但指针本身指向哪个对象是可变的。

s = &b; //OK
t = &a; //OK

上面的代码完全可以正常运行。

常量指针

那怎么让指针自己不可变呢?嗯,这里事情逐渐变得复杂了起来。

const int a = 10;
int b = 10;
int *const s = &a; //Error
int *const t = &b;
const int *const s1 = &a;
const int *const t1 = &b;

Wait Wait Wait 这有点太复杂了,我们还是一行行看。

注意到了吗?我们使用了 *const,它表示定义一个常量指针。顾名思义,指针本身是常量,不能变(不能改变保存的位置,即不能修改它指向的对象是哪一个)。

现在来看代码:

  • 第三行,它定义了一个本身是常量的指针(而认为指向的对象是可变的),但是却绑定到了不可变常量 a,因此报错(上文已经强调过,不能认为不可变的东西可变)
  • 第四行,它定义了一个本身是常量的指针,绑定到了变量 b,没问题。
  • 第五行,它定义了一个本身是常量的指针,且认为指向对象不可变,绑定到了不可变常量,没问题。
  • 第六行,它定义了一个本身是常量的指针,且认为指向对象不可变,绑定到了变量,没问题

为了更加清晰说明什么叫本身不可变,什么叫认为指向对象不可变,再给出以下代码:

const int c = 11;
t = &c; //Error
s1 = &c; //Error
t1 = &c; //Error
*t = 12;
*s1 = 12; //Error
*t1 = 12; //Error

仔细想想为什么那些行会报错吧。

  • 本身不可变的指针,不可以重新指向其它位置。因此 2,3,4 行报错
  • 之前提到过,如果指针认为自己指向的对象不可变,那么它解引用后不可变,所以 6,7 行报错

好好思考一下,分清楚什么是本身不可变,什么是认为指向的对象不可变(解引用后不可变)。

Tips:还是再回忆下 const 修饰的真正含义吧。如果认为指向对象不可变,那么这和指向对象实际是否可变没有任何关系。

如果你理解了,真得好好夸夸自己,连这么复杂的东西都搞懂了!

顶层与底层

我们把本身不可变的 const,称作顶层 const;认为指向对象不可变的 const,称作底层 const。

从之前的讲解中,我们不难得到推论:

  • 引用本身不可变,所以只有认为不可变的底层 const 存在。
  • 对于指针,如果放在距离变量名远的地方,那么是底层;距离变量名近的地方,是顶层
  • 底层 const 只和指针、引用有关,而顶层 const 可以修饰大部分对象

很好。接下来我们就要涉及一些更加深入的话题了。

我们曾经在特殊用法那里提过一嘴自动转换。众所周知,在执行赋值操作时,可能会进行自动转换。而变量可以转换为常量,常量也可以转换为变量:

int a = 10;
const int b = a; //b 被顶层 const 修饰,它本身不可变
int c = b;

但对于指针和引用来说,事情就更加复杂了。

当你赋值,涉及指针、引用时,源和目标的顶层 const 可以不同,但顶层(决定本身是否可变)必须满足自动转换(不可变拷贝到可变)。注意上面代码第二行,和下面代码最后一行。

const int d = 20;
const int *const p1 = &b;
const int *p2 = &d;
p1 = p2;//Error
p2 = p1;//现在,p1,p2 都指向 b。

对于底层 const,这决定了源、目标认为其所指向的对象是否可变。在此过程中,源和目标的底层 const 可以不同,但是底层(指向对象是否可变)必须满足自动转换(可变拷贝到不可变)

int e = 1;
int *p3 = &d; //Error
int *p4 = &e;
p2 = p4; //为变量的指针增加不可变修饰

但注意一下,上面只是赋值操作,如果是新创建指针,那么顶层 const 无所谓(正如之前所述):

const int *const m = p1;
const int *m1 = p1;

我知道你确实有点晕了。

总结一下,修改指针操作时,看等号左侧是否有顶层 const 很重要,有顶层 const 就不能修改;而任何操作时,都有必要去检查下等号右侧的底层 const,如果有,那么左边也必须有,否则左侧随意。

试试这样想吧:const 就是一种修饰。指针是瓶子的标签,你可以让瓶子(对象)本身不可变(顶层 const 修饰),但这样你必须在标签(指针)上写上“别动瓶子”(底层 const 修饰)。如果你看到了“别动”的标签(底层 const 修饰的指针),想根据这个标签给瓶子再贴一个标签,或者把别的瓶子上的标签移过来(创建新指针/修改旧指针),那么另一个标签上也得写“别动”(底层 const 修饰)。

如果你的标签上没有“别动”(没有底层 const),说明瓶子本身一定是可以动的(没有顶层 const),所以新创建的标签写不写“别动”都无所谓(有没有底层 const 并没有关系)。

而如果一个标签是强力胶,撕不下来(指针有顶层 const 修饰),那么它就不能移动。但是你还是可以根据这个标签,移动其它可以移动的标签(将其它无顶层 const 修饰的指针,赋值为它),或者创建一个新的标签,是否为强力胶都可以(创建新的指针时,顶层 const 修饰并不重要)。

用比喻来说,顶层 const 决定了标签有没有强力胶;底层 const 决定我们是否认为瓶子能动。如果有强力胶,一个标签本身就不能移动了,但不影响其它标签。如果我们根据一个标签,不认为瓶子能动,那么也就没办法再贴上能动的标签了。

注意了,我这里一直强调根据某个标签,是因为这是在指针的语境下来说的,我们必须根据指针来进行寻找对象、赋值等操作,而不是直接操作对象。好好想想,上文所述“根据某个标签”,指的就是赋值等号右侧的内容。

你也可以配上下面的表格举例,一起理解(注意代码是无法运行的,这里只是为了看清楚而写出了每个类型,所以没有用赋值的等号):

  • 当操作为:创建指针,并赋值时:

    • int *p <- const int a Error 试图认为不可变的常量可变
    • const int *p <- const int a OK
    • const int *p <- int a OK
    • int *p1 <- int *const p OK
    • int *const p1 <- int *const p OK
    • const int *p1 <- int *const p OK
    • const int *p1 <- int *p OK
    • const int *const p1 <- int *p OK
    • const int *const p1 <- int *const p OK
    • const int *const p1 <- const int *const p OK
    • const int *p1 <- const int *const p OK
    • int *const p1 <- const int *p Error 试图根据指向不可变对象的指针,认为指向可变对象
    • int *const p1 <- const int *const p Error 试图根据指向不可变对象的指针,认为指向可变对象
  • 当操作为:修改左侧已经创建的指针时:
    • int *p <- const int a Error 试图认为不可变的常量可变
    • const int *p <- const int a OK
    • const int *p <- int a OK
    • int *p1 <- int *const p OK
    • int *const p1 <- int *const p Error 试图修改不可变的指针
    • const int *p1 <- int *const p OK
    • const int *p1 <- int *p OK
    • const int *const p1 <- int *p Error 试图修改不可变的指针
    • const int *const p1 <- int *const p Error 试图修改不可变的指针
    • const int *const p1 <- const int *const p Error 试图修改不可变的指针
    • const int *p1 <- const int *const p OK
    • int *const p1 <- const int *p Error 试图根据指向不可变对象的指针,认为指向可变对象 / 试图修改不可变的指针
    • int *const p1 <- const int *const p Error 试图根据指向不可变对象的指针,认为指向可变对象 / 试图修改不可变的指针

OKOKOK,我知道这很难,但是再读一遍,理解一下指针的 const 修饰吧。

哦对了,赋值记得保证除了 const 外的数据类型一致。别忘了这个基础。

如果你连这个都掌握了,你已经对 const 有了相当程度的认识。恭喜!

下一篇,我们将离开指针的苦海,继续讲解 C++。当然,也是奶奶级的拆解哦。

奶奶都能看懂的 C++ —— const 限定符与指针的更多相关文章

  1. 【C】——指针与const限定符

    const限定符和指针结合起来常见的情况有以下几种. const int *a; int const *a; 这两种写法是一样的,a是一个指向const int型的指针,a所指向的内存单元不可改写,所 ...

  2. 小学生都能看懂的FFT!!!

    小学生都能看懂的FFT!!! 前言 在创新实践重心偷偷看了一天FFT资料后,我终于看懂了一点.为了给大家提供一份简单易懂的学习资料,同时也方便自己以后复习,我决定动手写这份学习笔记. 食用指南: 本篇 ...

  3. 机器学习敲门砖:任何人都能看懂的TensorFlow介绍

    机器学习敲门砖:任何人都能看懂的TensorFlow介绍 http://www.jiqizhixin.com/article/1440

  4. 只要听说过电脑的人都能看懂的网上pdf全书获取项目

    作者:周奇 最近我要获取<概统>的教材自学防挂科(线代已死),于是我看到 htt链ps:/链/max链.book接118接.com接/html/2018/0407/160495927.sh ...

  5. 55张图吃透Nacos,妹子都能看懂!

    大家好,我是不才陈某~ 这是<Spring Cloud 进阶>第1篇文章,往期文章如下: 五十五张图告诉你微服务的灵魂摆渡者Nacos究竟有多强? openFeign夺命连环9问,这谁受得 ...

  6. 搭建分布式事务组件 seata 的Server 端和Client 端详解(小白都能看懂)

    一,server 端的存储模式为:Server 端 存 储 模 式 (store-mode) 支 持 三 种 : file: ( 默 认 ) 单 机 模 式 , 全 局 事 务 会 话 信 息 内 存 ...

  7. C++中的常量(一) const限定符

    最近在重新看<<C++ Primer>>,第一遍的时候const和constexpr看得并不太懂,这次又有了些更新的理解,当然可能仍然有许多不对的地方... 首先,const限 ...

  8. C++之const限定符

    作者:tongqingliu 转载请注明出处: C++之const限定符 const初始化 const的特点: 用const加以限定的变量,无法改变. 由于const对象定义之后就无法改变,所以必须对 ...

  9. C++之const限定符(顶层const,底层const)

    作者:tongqingliu 转载请注明出处:http://www.cnblogs.com/liutongqing/p/7050815.html C++之const限定符(顶层const,底层cons ...

  10. Spline样条函数 //C++关键字:operator // 重载函数 // 隐含的this指针 // 指针和const限定符

    在数学学科数值分析中,样条是一种特殊的函数,由多项式分段定义.样条插值是使用一种名为样条的特殊分段多项式进行插值的形式.由于样条插值可以使用低阶多项式样条实现较小的差值误差,这样就避免了使用高阶多项式 ...

随机推荐

  1. Car Kit重构车机开发体验,让车载应用开发驶入快车道

    在智能座舱成为汽车行业"新四化"核心战场的今天,开发者们正面临这样的挑战:如何让手机应用快速适配车机场景?如何实现手机与车机无感流转?如何在保障驾驶安全的前提下提供沉浸式交互体验? ...

  2. Win10远程桌面出现“身份验证错误,CredSSP加密Oracle修正”解决方法 --九五小庞

    Windows10更新1803后,远程桌面就会连接失败,显示如下: error.jpg 这就比较尴尬了. 根据微软官方的说法是更改了安全策略 https://support.microsoft.com ...

  3. 基础篇:6.8)形位公差-公差带 Tolerance Zone

    本章目的:了解14个形位公差的公差带形状,其从属关系. 1.定义 公差带-实际被测要素允许变动的区域. 它体现了对被测要素的设计要求,也是加工和检验的根据. 2.公差带四大特征-形状.大小.方向.位置 ...

  4. Win11正式版玩红警2出现卡住不动的问题

    近来有深度系统的用户在win11正式版电脑玩红警2的时候,玩的好好的,突然就卡住不动了,这要怎么办呢?针对这个问题,本文中深度技术小编就带来详细的解决方案,和大家分享一下,希望可以帮助到有需要的小伙伴 ...

  5. 05Java基础语法之流程控制

    流程控制 用户交互Scanner 之前学得基本语法中并没有实现程序和人交互的场景,但是Java给我们提供了一个工具类,我们可以通过它来获取用户的输入,它就是Scanner. 使用它时,需要导入java ...

  6. 从零开始实现简易版Netty(五) MyNetty FastThreadLocal实现

    从零开始实现简易版Netty(五) MyNetty FastThreadLocal实现 1. ThreadLocal介绍 在上一篇博客中,lab4版本的MyNetty对事件循环中的IO写事件处理进行了 ...

  7. 软RAID(Software RAID)和硬RAID(Hardware RAID)区别

    在Linux存储系统中,软RAID(Software RAID)和硬RAID(Hardware RAID)在实现方式.性能.应用场景及管理上存在本质差异.以下从五个维度详细解析其区别: ⚙️ 一.实现 ...

  8. SQL Server 分组排序后取第N条数据(或前N条)

    节选自 https://blog.csdn.net/cxu123321/article/details/92059001 分组取前N条数据SQL SELECT * FROM( SELECT ROW_N ...

  9. kafka 副本集设置和理解

    转载请注明出处: 最近在做集群高可用验证的时候,遇到了一个kafka 副本集高可用的问题,在这里分析总结一下. 当前的部署情况是kafka集群有三个节点:在做集群高可用验证的时候,先shutdown一 ...

  10. C#脚本化(Roslyn):如何在C#脚本中引入nuget包

    假设我们开发了一个C#脚本编辑器,利用Roslyn去执行用户所编写的脚本.这时候,如果用户想要引用一个nuget包,应该如何实现呢? 我们想要引用nuget包的话,只要能得到nuget包及其依赖包的所 ...