2.4 const限定符

本文探讨了const关键字在编程中的应用,包括变量值不可更改的特性,初始化规则,以及如何在多个文件间共享const对象。重点讲解了const对象、引用和指针的使用限制,以及constexpr和常量表达式的概念。
摘要由CSDN通过智能技术生成

const简介

  • const关键字对变量的类型加以限制,使得它的值不能被改变。
    • 例如,用一个变量表示缓冲区的大小。
    • 使用变量的好处是当我们觉得缓冲区大小不合适时,很容易对其进行调整。
    • 另一方面,也随时警惕防止程序一不小心改变了这个值。
  • 因为const对象一旦创建后其值就不能再改变,所以const对象必需初始化。
初始化和const
  • 与非const类型相比,const类型能完成其中的大部分操作,主要的限制就是只能在const类型的对象上执行不改变其内容的操作。
默认状态下,const对象仅在文件内有效
  • 当以编译时初始化的方式定义一个const对象时,编译器将在编译的过程中把用到该变量的地方都替换成相应的值。为了执行上述替换,编译器必需知道变量的初始值。
  • 如果程序包含多个文件,则每个用了const对象的文件都必需得能访问到它的初始值。要做到这一点,就必需在每一个用到变量的文件中都有对它的定义。为了支持这一用法,同时避免对同一变量的重复定义,默认情况下,const对象被设置为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同的文件中分别定义了独立的变量。
  • 某些时候有这样一种const变量,它的初始值不是一个常量表达式,但又有必要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量,而是希望只在一个文件中定义const,而在其它多个文件中声明并使用它。
  • 解决方法是对于const变量,不管是声明还是定义都添加extern关键字。
//file_1.cc定义并初始化了一个常量,该常量能被其它文件访问
extern const int bufSize = fcn();
//file_1.h头文件
extern const int bufSize;
  • file_1.cc定义并初始化了bufSize。因为这条语句包含了初始值,所以它是定义。因为bufSize是一个常量,必需用extern加以限定使其能被其它文件使用。
  • file_1.h头文件中的声明也由extern做了限定,其作用是指明bufSize并非本文件独有,它的定义将在别处出现。

如果想在多个文件之间共享const对象,必需在变量的定义之前添加extern关键字

const的引用

  • 可以把引用绑定到const对象上,就像绑定到其它对象上一样,称之为对常量的引用。
  • 与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。
const int ci = 1024;
const int &r1 = ci;//正确:引用及其对应的对象都是常量
r1 = 42;//错误:r1是对常量的引用
int &r2 = ci;//错误:试图让一个非常量引用指向一个常量对象
初始化和对const的引用
  • “常量引用”是“对const的引用”的简称。
  • 引用类型必需与其所引用的对象的类型一致,但是有两个例外。
  • 第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。允许一个常量引用绑定非常量的对象、字面值,甚至是一个表达式。
int i = 42;
const int &r1 = i;//允许将const int&绑定到一个普通int对象上
const int &r2 = 42;//正确:r1是一个常量引用
const int &r3 = r1 * 2;//正确:r3是一个常量引用
int &r4 = r1 * 2;//错误:r4是一个普通的非常量引用
  • 当一个常量引用被绑定到另外一种类型上时,发生了什么
double dval = 3.14;
const int &ri = dval;
  • 此处ri引用了一个int型的数,对ri的操作应该是整数运算,但dval是一个双精度浮点数。因此,为了确保让ri绑定一个整数,编译器把上述代码变成了以下形式
const int temp = dval;//由双精度浮点数生成一个临时的整型常量
const int &ri = temp;//让ri绑定这个临时量
  • 在这种情况下,ri绑定了一个临时量对象。所谓临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。
  • 当ri不是常量时,执行类似上面的初始化行为是非法的。
对const的引用可能引用一个并非const的对象
  • 常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因此对象也可能是个非常量,所以允许通过其它途径改变它的值
int i = 42;
int &r1 = 1;//引用ri绑定对象i
const int &r2 = i;//r2也绑定对象i,但是不允许通过r2修改i的值
r1 = 0;//r1并非常量,i的值修改为0
r2 = 0;//错误:r2是一个常量引用
  • r2绑定整数i是合法的行为。然而,不允许通过r2修改i的值。尽管如此,i的值仍然允许通过其它途径修改,既可以直接给i赋值,也可以通过像r1一样绑定到i的其它引用来修改。

指针和const

  • 类似于常量引用,指向常量的指针不能用于改变其所指对象的值。想要存放常量对象的地址,只能使用指向常量的指针
const double pi = 3.14;//pi是个常量,它的值不能改变
double *ptr = π//错误:ptr是一个普通指针
const double *cptr = π//正确:cptr可以指向一个双精度常量
*cptr = 42;//错误:不能给*cptr赋值
  • 指针的类型必需与其所指对象的类型一致,但是有两个例外。第一种情况就是允许令一个指向常量的指针指向一个非常量对象
double dval = 3.14;//dval是一个双精度浮点数,它的值可以改变
cptr = &dval;//正确:但是不能通过cptr改变dval的值
  • 和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅是要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其它途径改变。
const指针
  • 指针是对象而引用不是,因此就像其它对象类型一样,允许把指针本身定为常量。
  • 常量指针必须初始化,且一旦初始化完成,则它的值,即存放在指针中的那个地址,就不能再改变了。
  • 把*放在const前用以说明指针是一个常量,表示不变的是指针本身的值而非指向的那个值。
int errNumb = 0;
int *const curErr = &errNumb;//curErr将一直指向errNumb
const double pi = 3.14159;
const double *const pip = π//pip是一个指向常量对象的常量指针
  • 想要弄清楚这些声明的含义的最有效的方法是从右向左阅读。此例中,离curErr最近的符号是const,意味着curErr本身是一个常量对象,对象的类型是由声明符的其余部分确定。声明符中的下一个符号是*,意思是curErr是一个常量指针。最后,该声明语句的基本数据类型部分确定了常量指针指向的是一个int对象。
  • 指针本身是一个常量并不意味着不能通过修改指针修改其所指对象的值。能否这样做完全依赖于所指对象的类型。
if(*curErr){
	*curErr = 0;//正确:把curErr所指的对象重置
}
顶层const
  • 用名词顶层const表示指针本身是个常量,而用名词底层const表示指针所指的对象是一个常量
  • 顶层const可以表示任意的对象是常量,这一点对任意数据类型都适用,如算数类型、类、指针等。
  • 底层const则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层const也可以是底层const。
int i = 0;
int *const pi = &i;//不能改变pi的值,是一个顶层const
const int ci = 42;//不能改变ci的值,是一个顶层const
const int *p2 = &ci;//允许改变p2的值,是一个底层const
const int *const p3 = p2;//靠右的是顶层const,靠左的是底层const
const int &r = ci;//用于声明引用的const都是底层const
  • 当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响
i = ci;//正确:拷贝ci的值,ci是一个顶层const,对此操作无影响
p2 = p3;//正确:p2和p3指向的对象类型相同,p3顶层const的部分不影响
  • 指向拷贝操作并不会改变被拷贝对象的值,因此,拷入和拷出的对象是否是常量都没什么影响。
  • 而底层const的限制却不容忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须有相同的底层const资格,或者两个对象的数据类型必需能够转换。一般来说,非常量可以转换为常量,反之则不行。
int *p = p3;//错误:p3包含底层const的定义,而p没有
p2 = p3;//正确:p2和p3都是底层const
p2 = &i;//正确:int*能转换成const int*
int &r = ci;//错误:普通的int&不能绑定到int常量上
const int &r2 = i;//正确:const int&可以绑定到一个普通int上

constexpr和常量表达式

  • 常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
  • 一个对象是不是常量表达式由它的数据类型和初始值共同决定
const int max_files = 20;//max_files是常量表达式
const int limit = max_files +1;//limit是常量表达式
int staff_size = 27;//staff_size不是常量表达式
const int sz = get_size();//sz不是常量表达式
  • 尽管staff_size的初始值是个常量表达式,但由于它的数据类型只是一个普通的int而非const int,所以它不属于常量表达式。
  • 尽管sz本身是一个常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。
constexpr变量
  • 允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。
  • 声明为constexpr类型的变量一定是一个常量,且必需用常量表达式初始化
constexpr int mf = 20;//20是常量表达式
constexpr int limit = mf +1;//mf+1是常量表达式
constexpr int sz = size();//只有当size是一个constexpr函数时才是一条正确的声明语句

一般来说,如果认定变量是一个常量表达式,那就把它声明成constexpr类型

字面值类型
  • 常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必需有所限制。因为这些类型一般比较简单,值也显而易见,就把它们称为"字面值类型"
  • 算术类型、引用和指针都属于字面值类型。自定义类、IO库、string类型则不属于字面值类型,也就不能被定义成constexpr。
  • 尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或0,或者是存储于某个固定地址中的对象。
    • 函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。
    • 定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。
    • 允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量也一样有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr指针也指向这样的变量。
指针和constexpr
  • 在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关。
const int *p = nullptr;//p是一个指向整型常量的指针
constexpr int *q = nullptr;//q是一个指向整数的常量指针
  • p和q的类型相差甚远,p是一个指向常量的指针,而q是一个常量指针,其中的关键在于constexpr把它所定义的对象置为了顶层const
  • 与其它常量指针类似,constexpr指针既可以指向常量也可以指向一个非常量
const int *np = nullptr;//np是一个指向整数的常量指针,其值为空
int j = 0;
constexpr int i = 42;//i的类型是整型常量
//i和j都必须定义在函数体之外
constexpr const int *p = &i;//p是常量指针,指向整型常量i
constexpr int *p1 = &j;//p1是常量指针,指向整数j
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值