我们可以把引用绑定到const对象上,就像是对其它的对象引用一样。我们称之为对常量的引用。然而,对常量的引用不能修改被引用对象的值。
const int a = 10;
const int &b = a; // 正确:引用和被引用对象都是常量
b = 1; // 错误:试图修改被引用的常量对象的值
int &c = a; // 错误:试图让一个非常量对象引用一个常量对象
第四个因为不能对a赋值,所以试图通过引用对其赋值是不合法的。假设初始化成立的话,那么便可以根据引用来修改a的值,这显然是不成立的。
在C++语言中,除两种例外情况,其他引用的类型都要和与之绑定的对象严格匹配,如int型的引用只能绑定int型的对象;并且引用不能直接与字面值常量或表达式结果绑定。
其中一种例外情况是:初始化常量引用时,允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式。例如:
int a = 42;
const int &b = i; // 正确:允许将一个const int&绑定到一个普通的int对象上
const int &c = 42; // 正确:c是一个常量引用
const int &d = a * 2; // 正确:d是一个常量引用
int &d = a * 2 // 错误:d是一个普通的int型非常累引用
我们用一个例子来帮助我们理解。
double a = 3.3;
const int b = a;
此处b为int型引用,它的操作对象应该为int型,然而a却是一个双精度类型。编译器如何工作的呢?
double a = 3.3;
const int tmp = a; // 这里会有一个隐式转换,tmp = 3
const int &b = tmp;
编译器创建了一个临时对象tmp,即b是对临时对象tmp的引用,这样就可以理解了吧。
那么如果b是非常量引用呢?根据上面的过程,我们假设编译器这样工作
double a = 3.3;
int tmp = a; // 这里会有一个隐式转换,tmp = 3
int &b = tmp;
b是对tmp的引用,那么如果我们对b赋值也就是对tmp赋值,那么对临时对象赋值有什么用呢?它不会修改a的值,这样做无意义,程序员也不希望这样,因此就是非法的了。
1.1 指向指针的引用
引用本身不是一个对象,因此不能定义指向引用的指针。但是指针是对象,所以存在指向指针的引用。
int a = 0;
int *p = nullptr;
int *&r = p; // r是对指针p的引用
锦囊妙计:要理解r的类型到底是什么,最简单的办法就是从右向左阅读r的定义。离变量名最近的符号对变量的类型有最直接的影响。因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中*说明r引用的是一个指针,最后,声明的基本数据类型部分指出r引用的是一个int指针。即面对一条比较复杂的指针或者引用的声明语句时,从右向左阅读有助于弄清楚它的真正含义。
我们知道,指针本身是不是个常量和指针所指向的对象是不是个常量是两个问题。用顶层const来表示指针本身是个常量,用底层const表示指针所指向的对象是个常量。
再广泛一点,顶层const表示本身是常量,任何数据类型都OK的。不严谨的说,除了指针外其它类型的const都是顶层const,指针既有顶层const,也有底层const。
int a = 0;
int const *b = &a; // 顶层const,它所指向的对象不能变,但是所指向的对象的值可以变
const int *c = &a; // 底层const,它所指向的对象可以改变,但是不能通过该指针的解引用来改
// 变所指向的对象的值
// 这里的不能通过该指针的解引用来改变所指向的对象的值不是值该对象的值不能改变
*c = 10; // 错误
a = 10; // 正确
2.1 顶层const和底层const的拷贝操作
int a = 0;
const int *b = &a; // 底层const
int* const c = b; // 顶层const
const int &d = a; // 用于引用的const都是底层const
const int e = 10;
当执行拷贝操作时,顶层const和底层const的区别非常的明显。对于顶层const来说并没有什么影响。
a = e; // 正确
c = b; // 正确,b和c指向的对象相同,所以说c指向的对象没有改变,对c的顶层const并没有什么
// 影响
执行拷入和拷出的操作并没有改变对象的值,因此拷贝对象是否为常量无所谓。
对于底层const来说,拷贝操作的两个对象必须都具备底层const的资格,或者之间能够进行类型转换。一般来说,非常量类型可以转换为常量类型,反之则不行。
int *f = b; // 错误,f不具备底层const资格
b = c; // 正确:b和c都具备底层const资格
/* 为什么这么说呢。首先c能赋值给b,那么他们指向的对象肯定是相同的,既然相同的那么b指向的对象是不是常量,既然是常量那么b是不是就具备了底层const资格了(指向的对象的值不变)*/
int *g = nullptr;
b = g; // 正确:int* 可以转换为const int*
int &h = e // 错误:int& 不能绑定到常量上
const int &i = a // 正确:const int& 可以绑定到int上(不懂就想想临时对象tmp的过程)
3.1 常量表达式
常量表达式是指值不会改变并且在编译过程中就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
const int a = 20; // a是个常量表达式
const int b = a + 1; // b是个常量表达式
int c = 27; // c不是常量表达式
const int d = getSize(); // d不是个常量表达式
尽管c的初始值是个字面值,但是由于它是int型,所以它不是常量表达式;尽管d本身是个常量,但是它的具体值直到运行阶段才能获知,因此它也不是个常量表达式。
3.2 constexpr变量
在一个复杂系统中,很难分辨一个初始值到底是不是常量表达式。当然可以定义一个const变量并把它的初始值设为我们认为的某个常量表达式,但在实际使用时,尽管要求如此却常常发现初始值并非常量表达式的情况。可以这么说,在此种情况下,对象的定义和使用根本就是两回事。
C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量是否是常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式来初始化。
constexpr int a = 20; // 20是常量表达式
constexpr int b = a + 1; // a + 1是常量表达式
constexpr int c = getSize(); // 只有getSize函数是constexpr函数时才是一条正确的语句。
锦囊妙计:一般来说,如果认定变量是一个常量表达式,那就把它声明为constexpr类型。
3.3 字面值类型
常量表达式的值需要再编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们成为“字面值类型”。
尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格的限制、一个constexpr指针的初始值必须死nullptr或者0,或者是存储于某个固定地址中的对象。
3.4 指针和constexpr
在constexpr声明中如果定义了一个指针,那么限定符constexpr仅对指针有效,与其所指的对象无关。即该指针为顶层const。