C++ 学习 关于const 上
前言
这篇blog 主要是想具体讲讲新学到的const 当然不止是const 而是基于这个const引申出来的指针和引用。还是需要捋一捋的,这还是有点困难的。
我会把每一节的重点都摘出来,放在前面~
1⃣首先讲讲const
2⃣const引用和const指针 ❗ 特别容易混淆
这边blog 常量指针/引用 指针/引用常量 详细讲的不错 可以看看~
const 限定符
🌈敲重点!
🍎因为const对象一旦创建后其值就不能再改变,所以==const对象必须初始化==
🍐只能在const类型的对象上执行不改变其内容的操作
有时我们希望定义这样一种变量,它的值不能被改变。例如,用一个变量来表示缓冲区的大小。使用变量的好处是当我们觉得缓冲区大小不再合适时,很容易对其进行调整。另一方面,也应随时警惕防止程序一不小心改变了这个值。为了满足这一要求,可以用关键字const对变量的类型加以限定:
1 | const int bufsize = 512;//输入缓冲区大小 |
这样就把 bufsize定义成了一个常量。任何试图为 bufsize赋值的行为都将引发错误:
1 | bufSize = 512;//错误:试图向const对象写值 |
因为const对象一旦创建后其值就不能再改变,所以==const对象必须初始化==。 一如既往,初始值可以是任意复杂的表达式:
1 | const int i = get_size();//正确:运行时初始化 |
初始化和const
正如之前反复提到的,对象的类型决定了其上的操作。与非 const类型所能参与的操作相比,const类型的对象能完成其中大部分,但也不是所有的操作都适合。主要的限制就是只能在const类型的对象上执行不改变其内容的操作。例如,const int和普通的int一样都能参与算术运算,也都能转换成一个布尔值,等等。
在不改变 const 对象的操作中还有一种是初始化,如果利用一个对象去初始化另外一个对象,则它们是不是const都无关紧要:
1 | int i =42; |
尽管ci是整型常量,但无论如何ci中的值还是一个整型数。ci的常量特征仅仅在执行改变ci的操作时才会发挥作用。当用ci去初始化j时,根本无须在意ci是不是一个常量。拷贝一个对象的值并不会改变它,一旦拷贝完成,新的对象就和原来的对象没什么关系了。
默认情况下,const对象只在文件内有效
当以编译时初始化的方式定义一个const对象时,就如对bufsize的定义一样:
1 | const int bufSize = 512;//输入缓冲区大小 |
编译器将在编译过程中把用到该变量的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到bufsize的地方,然后用512替换。
为了执行上述替换,编译器必须知道变量的初始值。如果程序包含多个文件,则每个用了const对象的文件都必须得能访问到它的初始值才行。要做到这一点,就必须在每一个用到变量的文件中都有对它的定义(参见2.2.2节,第41页)。为了支持这一用法,同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
某些时候有这样一种const变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。相反,我们想让这类const对象像其他(非常量)对象一样工作,也就是说,只在一个文件中定义const,而在其他多个文件中声明并使用它。
解决的办法是,对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了:
1 | // file_1.cc定义并初始化了一个常量,该常量能被其他文件访问 |
如上述程序所示,file_1.cc定义并初始化了bufsize。因为这条语句包含了初始值,所以它(显然)是一次定义。然而,因为bufsize是一个常量,必须用extern加以限定使其被其他文件使用。
file 1.h头文件中的声明也由extern做了限定,其作用是指明bufsize并非本文件所独有,它的定义将在别处出现。
如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。
练习
1 | //练习2.26:下面哪些句子是合法的?如果有不合法的句子,请说明为什么? |
const int buf;
这个句子是不合法的。在C++中,const
关键字用于声明一个常量,它必须被初始化。因此,const int buf;
缺少初始化表达式,正确的声明应该是const int buf = 0;
。
int cnt = 0;
这个句子是合法的。它声明了一个整型变量cnt
并初始化为0。const int sz = cnt;
这个句子是不合法的。这里有两个问题:首先,const
变量必须在声明时初始化,不能在声明后赋值。其次,即使cnt
已经被声明并初始化,sz
作为const
变量也不能从非const
变量cnt
那里赋值。正确的做法是直接在声明时初始化,例如const int sz = 5;
。
++cnt; ++SZ;
首先,++cnt;
是合法的,它表示对变量cnt
进行自增操作。然而,++SZ;
是不合法的,因为SZ
没有被声明为一个变量,而且变量名通常不以大写字母开头,这是C++中常见的命名约定,尽管这不是语法错误。如果SZ
是一个未声明的变量,那么这个句子将导致编译错误。
关于const 引用
敲重点
🍌常量的引用 不能用作修改被绑定的对象
也就是说 原来的数据类型是const xx 类型 那么,对这个数据引用后就不能利用引用的变量去更改原有的值(好像有点绕 具体看下文)
❗注意 常量的引用 和 常量引用 的不同 一个是被引用的变量 一个是变量本身 下面看的时候要认真一些
常量的引用 数据类型需要一致 相当于对一个const类型的变量进行引用,这个时候就叫常量的引用
而常量引用 是一个引用类型的变量
🍑在初始化常量引用时允许用任意表达式作为初始值,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式。
🍊常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值:
可以把引用绑定到const对象上,就像绑定到其他对象上一样,我们称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:
1 | const int ci = 1024; |
因为不允许直接为ci赋值,当然也就不能通过引用去改变ci。因此,对r2的初始化是错误的。假设该初始化合法,则可以通过r2来改变它引用对象的值,这显然是不正确的。
常量引用是对const的引用
程序员们经常把词组“对 const的引用”简称为“常量引用”,这一简称还是挺靠谱的,不过前提是你得时刻记得这就是个简称而已。
严格来说,并不存在常量引用。因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于C+语言并不允许随意改变引用所绑定的对象,所以从这层意义上理解所有的引用又都算是常量。引用的对象是常量还是非常量可以决定其所能参与的操作,却无论如何都不会影响到引用和对象的绑定关系本身
初始化和对const 的引用
2.3.1节(第46页)提到,引用的类型必须与其所引用对象的类型一致,但是有两个例外。第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成(参见2.1.2节,第32页)引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式:
1 | int i =42;· |
要想理解这种例外情况的原因,最简单的办法是弄清楚当一个常量引用被绑定到另外一种类型上时到底发生了什么:
1 | double dval = 3.14; |
此处ri引用了一个int 型的数。对ri的操作应该是整数运算,但dval却是一个双精度浮点数而非整数。因此为了确保让ri绑定一个整数,编译器把上述代码变成了如下形式:
1 | const int temp =dval; |
接下来探讨当ri不是常量时,如果执行了类似于上面的初始化过程将带来什么样的后果。如果ri不是常量,就允许对ri赋值,这样就会改变ri所引用对象的值。注意,此时绑定的对象是一个临时量而非 dval。程序员既然让 ri引用dval,就肯定想通过ri改变dval的值,否则为什么要给ri赋值呢? 如此看来,既然大家基本上不会想着把引用绑定到临时量上,C++语言也就把这种行为归为非法。
对const的引用可能引用一个并非const 的对象
必须认识到,常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值:
1 | int i =42; |
r2绑定(非常量)整数i是合法的行为。然而,不允许通过r2修改i的值。尽管如此,i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过像r1一样绑定到i的其他引用来修改。
指针和const
🌈 敲重点
和上一节一样 常量指针和指向常量的指针 这俩是不一样的!
⛵指向常量的指针 指向常量的指针不能用于改变其所指向对象的值。存放常量对象的地址 只能使用指向常量的指针。和引用一样,指向常量的指针没有规定所指向的对象是一个常量,仅仅要求不能通过该指针改变对象的值,没有规定那个对象的值不能通过其他途径改变。
🌊 常量指针 必须初始化 一旦初始化, 他的值(也就是存放再指针的地址)不会再改变了 。*放在const之前说明指针是一个常量,也就是不变的是指针本身的值而不是指向的那个值
要想弄清楚声明的含义最行之有效的办法是从右向左阅读
指向常量的指针
指向常量的指针不能用于改变其所指向对象的值。存放常量对象的地址 只能使用指向常量的指针
带有前缀 const
1 | const double pi = 3.14;// pi是个常量,它的值不能改变 |
指针类型不一定要与所致的对象一致
和引用一样,指向常量的指针没有规定所指向的对象是一个常量,仅仅要求不能通过该指针改变对象的值,没有规定那个对象的值不能通过其他途径改变。
常量指针
常量指针必须初始化 一旦初始化, 他的值(也就是存放再指针的地址)不会再改变了
*** 放在const之前说明指针是一个常量,也就是不变的是指针本身的值而不是指向的那个值**
1 | int errNumb = 0; |
如同2.3.3节(第52页)所讲的,要想弄清楚这些声明的含义最行之有效的办法是从右向左阅读。此例中,离curErr最近的符号是const,意味着curErr本身是一个常量对象,对象的类型由声明符的其余部分确定。声明符中的下一个符号是*,意思是 curErr是一个常量指针。最后,该声明语句的基本数据类型部分确定了常量指针指向的是一个int对象。与之相似,我们也能推断出,pip是一个常量指针,它指向的对象是一个双精度浮点型常量。
指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型。例如,pip是一个指向常量的常量指针,则不论是pip所指的对象值还是pip自己存储的那个地址都不能改变。相反的,curErr指向的是一个一般的非常量整数,那么就完全可以用curErr去修改errNumb的值:
1 | *pip = 2.72; |
练习
🐋 ps 对于不确定的式子 可以代入代码敲一敲
1 | //练习2.27:下面的哪些初始化是合法的?请说明原因。 |
🔑 答案答案
1 | //练习2.27:下面的哪些初始化是合法的?请说明原因。 |
1 | //练习2.28:说明下面的这些定义是什么意思,挑出其中不合法的。 |
1 | //练习2.29:假设已有上一个练习中定义的那些变量,则下面的哪些语句是合法的?请说明原因。 |
完结 欢迎批评指正~