const限定符
不能被修改的变量,使用const
。
因为const
变量在创建后不能被修改,它必须被初始化。同样,初始化器可以是任意复杂表达式。
const int i = get_size(); // initialized at run time
const int j = 42; // initialized at compile time
const int k; // error
const对象默认是Local to File
如果const对象在从一个编译时常量初始化,比如const int bufSize = 512;
,编译器在编译时一般用值直接替换它。
为了去替换,编译器需要看到变量的初始化器。如果我们的程序有多个文件,需要使用extern
,才能使其他文件适应该对象。
(在声明、定义都得使用extern
)
// file.cc
extern const int bufSize = fcn();
// file.h
extern const int bufSize;
也就是说,为了在多个文件共享const变量,需要使用extern。
对const的引用
我们可以绑定一个引用到const类型对象,即对const的引用(reference to const)。
不同的常规引用,对const的引用是不能用作修改绑定的对象。
const int ci = 1024;
const int &r1 = ci; // both reference and underlying object are const
r1 = 42; // error
int &r2 = ci; // error: non const reference to const object
const对象的引用,也必须是const的。
C++程序员一般把
对常量的引用(reference to const)
缩写成常量引用(const reference)
。严格的说,不存在常量引用,因为引用不是对象。但是,因为不能将引用指向其他对象,在某种意义上,引用都是const的。至于引用是指向const对象,还是非const对象, 影响的是我们能对引用,而不是引用绑定的对象本身。
对const的引用的初始化
前面我们提到
除了两个特殊情况(以后再讲!),引用的类型和该引用指向对象的类型必须完全匹配。
第一个特殊情况就是:我们可以把对常量的引用
初始化成任意能转型成引用对象类型的表达式。
特别地,可以绑定一个对const的引用到一个非const对象,甚至是字面常量或者表达式。
int i = 42;
const int &r1 = i; // ok
const int &r2 = 42; // ok
const int &r3 = r1 * 2; // ok
int &r4 = r * 2; // error
我们可以通过对常量的引用
绑定不同类型的对象(只要能转型):
double dval = 3.14;
const int &ri = dval;
这里很有意思:因为ri
是整型的,dval
是浮点型的,而类型决定操作,为了确保r1
是绑定到int
,
编译器一般会把上面代码进行转化:
const int temp = dval;
const int &ri = temp;
于是,ri
实际上绑定的是一个临时对象;也计算编译器创建的未命名对象。
现在我们可以考虑,如果ri
不是const的,且允许上面初始化(double给int引用):
我们通过引用修改的是临时对象,不会反映到原对象。因此,这个语法是不合法的。
对const的引用可以指向非const对象
int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0;
r2 = 0; // error
绑定r2
到非const的i是合法的;但我们不能通过r2修改i的值。可还是能被修改。
再次强调,对const的引用只是限制了我们能对该引用做什么,并不意味着其底层绑定的对象本身需要是const的。 如果底层对象是非const的,它还是能改变。
指针和const
类似于对const的引用,对const的指针(pointer to const)不能用来去改变其指向的对象。 但是,该指针本身是可以多次被赋值的。只是,不能用该指针去改变其指向的对象
const对象的地址只能存储在对const的指针
。
const double pi = 3.14;
double *ptr = π // error
const double *ptr2 = π // ok
*ptr2 = 42; // error
同样,前面我们提到:
除了两个特殊情况(以后再讲!),指针的类型和该指向指向对象的类型必须完全匹配。
第一个特殊情况就是,对const的指针可以指向非const对象,即存储非const对象的地址。
double dval = 3.14;
ptr2 = &dval; // ok
类似对const的引用,对const的指针并不能说明指针指向的对象是否为const,只是限制我们能对该指针做什么。 (能再次赋值,不能通过它改变指向的对象)
const指针
指针是对象,其本身可以是const的。类似其它const对象,const指针(const pointer)必须被初始化, 且之后值(存储的地址)不能被修改。
int errNumb = 0;
int *const curerr = &errNumb; // curerr will always point to errNumb
*curerr = 42; // ok; the underlying object can be changed
我们还是从右到左理解这个声明:靠近变量curerr最近的是const
,因此它是const的,这意味着curerr本身是
const对象;该对象的类型由剩下的声明告知,下一个标识是*
,表明是个const指针;最后是声明的基本类型,表明这个
const指针指向的是int
。
const指针并不说明我们能否改变指针指向的底层对象;只是限制我们不能对指针本身的修改。
顶层const
正如我们看到的,指针是否为const的 和 指针能否指向const对象 是不同的。
我们使用顶层const(top-level const)表示指针本身是const的;使用低层const(low-level const)表示能指向 const对象的指针(是能指向const,而不是必须去指向const)。
但是,需要注意的是,对于基本内置类型,
int const x1 = 3;
const int x2 = 3;
是等价的。参见const int = int const?。
同时,显然int &const r
这样是错误的;因为引用本身不能是const的。
顶层const可以引用在任何对象(再次强调,引用不是对象),而低层const只能出线在指针、引用这样的复合类型。指针可以同时有 顶层const和低层const。
int i, *const cp; // illegal, cp must initialize.
int *p1, *const p2; // illegal, p2 must initialize.
const int ic, &r = ic; // illegal, ic must initialize.
const int *const p3; // illegal, p3 must initialize.
const int *p; // legal. a pointer to const int.
这样我们在赋值类型匹配时,需要明确:
顶层const在赋值时的const属性可被忽略;而低层const在赋值时const属性不能被忽略。
constexpr和常量表达式
常量表达式(constant expression)是在编译时就已经确定,且无法修改值的表达式。
const int sz = get_size()
就不是常量表达式,因为它不能在编译时确定。
int staff = 27
也不是常量表达式,必须得有const。
大型系统里有时很难确定某个初始化器是不是常量表达式(正如前面看的的,仅有const也不行);在
C++ 11的新标准下,我们通过constexpr
来让编译器去检查,当然,它是隐式的const。
constexpr int mf = 20; // 20 is a constant expression constexpr
int limit = mf + 1; // mf + 1 is a constant expression constexpr
int sz = size(); // ok only if size is a constexpr function
虽然我们可以将指针和引用定义成constexpr,但有很多限制: 初始化constexpr指针的只能是字面常量0或nullptr;或是固定地址的对象。
我们知道,定义在函数内的变量不可能存储在固定地址;因此,我们不能使用constexpr去指向这样的 变量。另一方面,定义在函数外对象的地址是个常量表达式,因而可以用作初始化constexpr指针。
定义在函数外的对象,有固定地址。
constexpr
就像一个顶层const,constexpr const int *p = ...
。
关于字面类型(literal type)
因为常量表达式可以在编译期间确定,因此它的类型有所限制。我们把可以用作constexpr
声明的称为字面类型
(literal type),因为它们简单到足以有字面值(litral value)。
目前我们使用的,算术值,引用,指针都是字面类型。而像IO库以及string等都不是。在类
这一章,我们将看到
更多的字面类型。