[C++] const 限定符

const限定符

因为const对象一旦创建后其值就不能再改变,因此const对象必须初始化

const int i = foo();  // 运行时初始化

const int j = 1;     // 编译时初始化

如果利用一个对象初始化另外一个对象,则它们是不是const都无关紧要。

int i = 42;

const int ci = i;

int j = ci;

通过以上初始化操作可知:const的常量特征仅仅在执行改变其值的操作时才会发挥作用

默认情况下,const对象被设定为仅在文件内有效。当多个文件出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量
如果想让const对象只在一个文件中定义,而在其他多个文件中声明并使用,则需要对于const变量不管是声明还是定义都添加extern关键字

extern const int ec = foo(); // in .cpp
extern const int ec; // in .h

const引用

常量引用,和普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象

const int ci = 1024;
const int &r1 = ci;
r1 = 42; // x r1是对常量的引用
int &r2 = ci; // x 试图让一个非常量引用指向一个常量对象

const引用初始化

允许为一个常量引用绑定非常量的对象,字面值,甚至是个一般表达式

int i = 42;
const int &r1 = i;
const int &r2 = 42;
const int &r3 = r1 * 2;

当一个常量引用被绑定到另外一种类型上时到底发生了什么呢?如下所示:

double dval = 3.14;
const int &ri = dval;

此时ri引用一个int型的数,对ri的操作应该是整数运算,但dval却是一个双精度浮点数而非整数。因此为了确保让ri绑定一个整数,编译器将上述代码变成以下形式:

const int temp = dval;
const int &ri = temp;

这种情况下,ri绑定的是一个临时量对象,这个临时量对象就是当编译器需要一个空间来暂存表达式求值结果时临时创建的一个未命名的对象。在我认为这是在编译器看来是合法的,实际运用时只是起到了让const引用绑定到了一个字面值,丢失精度且没有意义。
const引用也可以引用一个非const的对象,所以允许通过其他途径改变const对象的值。

int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0;
r2 = 0; // x r2是一个常量引用 

 指针和const

指向常量的指针(pointer to const)不能用于改变其所指对象的值,要想存放常量对象的地址,只能使用指向常量的指针

const double pi = 3.14;
double *ptr = π // x ptr是一个普通指针
const double *cptr = π
*cptr = 42; // x 不能给*cptr赋值

通常情况下指针的类型必须与其所指对象的类型一致,但是也有例外情况是允许令一个指向常量的指针指向一个非常量对象。

double dval = 3.14;
cptr = &dval; // 正确,但是不同通过cptr改变dval的值

其实,所谓指向常量的指针或引用,不过是指针或引用“自以为是”罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象的值。

const指针

常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了
把*放在const关键字之前用以说明指针是一个常量,即不变的是指针本身的值而非指向的那个值

int errNumb = 0;
int *const curErr = &errNumb;
const double pi = 3.14159;
const double* const pip = π

在这个例子中.离curErr最近的符号是const,意味着curErr本身是一个常量对象,对象的类型由声明符的其余部分确定。
声明符中的下一个符号是*,意思是curErr是一个常量指针。
该声明语句的基本数据类型部分确定了常量指针指向的是一个int对象。
由此可得,pip是一个常量指针,它指向的对象是一个双精度浮点型常量。

顶层const

顶层const表示指针本身是个常量
底层const表示指针所指的对象是一个常量

int i = 0;
int *const p1 = &i; // 不能改变p1的值,顶层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; // x 因为p3是底层const,而p不是
p2 = p3; // p2和p3都是底层const
p2 = &i; // int*能转换成const int*
int &r = ci; // x 普通的int&不能绑定在int常量上
const int &r2 = i; // const int&可以绑定到一个普通的int上。

p3既是顶层const也是底层const,拷贝p3时可以不在乎它是一个顶层const,但是必须清楚它指向的对象是一个常量,因此,不能用p3去初始化p,因为p指向的是一个普通的(非常量)整数。另一方面,p3的值可以赋给p2,因为这两个指针都是底层const,尽管p3同时也是一个常量指针(顶层const),仅就这次赋值而言不会有什么影响。

// 练习2.27:下面的哪些初始化是合法的?请说明理由
// (a)
int i = -1, &r = 0;
// 不合法,引用r的赋值对象必须是一个对象

// (b)
int *const p2 =  &i2;
// 合法,p2是一个常量指针,初始化为i2对象的地址

// (c)
const int i = -1, &r = 0;
// 合法,对常量的引用对象可以是字面值。

// (d)
const int *const p3 = &i2
// 合法,

// (e)
const int *p1 = &i2;
// 合法

// (f)
const int &const r2;
// 不合法,

// (g)
const int i2 = i, &r = i;
// 合法。
// 练习2.28:说明下面的这些定义是什么意思,挑出其中不合法的
// (a)
int i, *const cp;
// 不合法,cp必须初始化

// (b)
int *p1, *const p2;
// 不合法,p2必须初始化

// (c)
const int ic, &r = ic;
// 不合法,ic必须初始化

// (d)
const int *const p3;
// 不合法,p3必须初始化

// (e)
const int *p;
// 合法
// 练习2.29 假设已有上一个练习中定义的那些变量,则下面的哪些语句是合法的?说明原因
// i:普通变量
// p1:普通指针
// p3:指向常量的常量指针。
// p2:常量指针
// ic:常量

// (a)
i = ic;
// 合法,

// (b)
p1 = p3;
// 不合法,const int 与 int类型不符

// (c)
p1 = ⁣
// 不合法,同b,类型不符

// (d)
p3 = ⁣
// 不合法,p3是常量指针,不能再赋值

// (e)
p2 = p1;
// 不合法,同d

// (f)
ic = *p3;
// 不合法,ic是个常量
// 练习2.30:对于下面的这些语句,请说明对象被声明成了顶层const还是底层const 
const int v2 = 0; // 顶层const 
const int *p2 = &v2, *const p3 = &i; // 底层const、顶层const

constexpr和常量表达式

常量表达式是指值不会改变并且在编译过程中就能得到计算结果的表达式。

字面值属于常量表达式。

常量表达式由它的数据类型和初始值共同决定

const int max_files = 20;
// yes

const int limit = max_files + 10;
// yes

int staff_size = 29;
// no

const int sz = get_size();
// no

constexpr变量

C++11规定:允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。

声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。

constexpr int mf = 20;
// yes

constexpr int limit = mf + 10;
// yes

const int sz = size();
// 只有当size是一个constexpr函数时,才是一条正确的声明语句

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

字面值类型

算术类型、指针和引用都属于字面值类型。

如果要把指针和引用声明为constexpr,则它们的初始值必须要个限制

一个constexpr指针的初始值必须是nullptr或0,或者是存储于某个固定地之中的对象。

一般来说定义在函数体之外的对象其地址固定不变,可以初始化constexpr指针。

需要注意的是:如果在constexpr声明中定义一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关。

const int *p = nullptr
// p是一个指向整型常量的指针

constexpr int *q = nullptr;
// q是一个指向整数的常量指针

可见constexpr把它定义的对象置为了顶层const。

同时,constexpr指针既可以指向常量也可以指向一个非常量。

constexpr 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

Tips:

1、指向常量的指针(pointer to const)要想存放变量的地址,必须使用指向常量的指针:const int *p。 const *

2、常量指针(const pointer)指针自身是一个对象,不可变,int *const p = &pi(常量指针必须初始化!!!)* const

3、指向常量的常量指针:const int* const p(不仅指针本身不可变,指针所指对象也不可变)const * const

4、对const的引用时注意存在临时量。

5、顶层const:指的是指针本身不可以被改变

6、底层const:指的是指针所指对象不可以被改变

7、用于声明引用的const都是底层const

8、顶层const的拷贝不受限制

9、底层const的拷贝的对象必须具有相同的底层const资格

10、非常量可以赋值给常量,反之则不行

11、指向空的指针可以用nullptr和0代替。

posted @ 2017-09-30 12:46  immjc  阅读(310)  评论(0编辑  收藏  举报