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等都不是。在这一章,我们将看到 更多的字面类型。