世间有许多亘古不变的真理,人类不得撼动亦无法改变它的存在。不断地认识与利用真理,恰是人类文明不断进步的表现。
而在 C++ 中,同样也存在许多不得变化、不可变化的事物。我们要想使它们变得真正不可变,就需要我们主动对其作出限制。不可变是一层很好的"保护膜",可以帮助我们预防修改了不当修改的数据,也可以提醒我们数据的不可变性质。
接下来,我们便了解一下变量的不可变形态--常量。
常量相当于不可更改其中内容的变量,我们只得使用其中的数据,却不得修改其中的数据:
const double pi = 3.141592653;
这是最经典的应用了--定义一系列数学上的常量,这样我们进行数学计算时便方便多了:
double r = 3.5;
const double pi = 3.141592653;
std::cout < "S = " << pi * r * r << std:;endl;
上面的程序演示的是圆的面积计算,其中变量r
是半径,常量pi
则是圆周率。
而要想定义一个常量,很简单,在类型名前加const
即可。请注意,const
总是应当在所有具体的类型前出现。例如:
const long long MAX_SIZE = 99999999999999999999L;
const
不仅可用于普通变量,也可用于指针与引用。这里既可以是指针或引用所指向的类型拥有 const
属性,也可以是指针或引用本身带有 const
属性。
我们先来看一下指针,const
在指针声明的不同位置有不同的用途:
int i = 1; // 一个变量
const int ci = 2; // 一个常量
const int *p1 = &ci; // 一个指向常量的指针
int *const p2 = &i; // 一个指向变量的常量指针
看起来是否很迷惑?不用慌,记住一句话,就能轻松区分它们:
星号(*)前的全是指向的对象的类型,星号(*)后的全是自己的类型。
什么意思呢?先看前一句话,星号前的类型是我们要指向对象的类型,它只与指向的对象有关系,不影响指针本身:
const int a = 3;
const int *p1 = &a; // 星号前为 const int,意即指向 const int
const char b = 's';
char const *p2 = &b; // 星号前为 char const,意即指向 const char
后一句话则是针对指针本身而言了,在星号后面写一个 const
,意味着指针本身变得不可修改:
int a = 3;
int *const p = &a; // 星号后为 const,这个指针就不可被更改指向谁了
当然了,也可以一起使用:
const int ca = 6;
const int *const cp = &ca; // 不仅指向的对象不可更改,自己也不可更改
引用相比较于指针要简单一些,因为引用实质上就是别名,为别名设置 const
与为引用到的对象设置 const
没有什么区别:
const int a = 3;
const int &ref = a; // 引用的对象即为常量
不过,非 const
的普通变量同样可被声明带有 const
的引用所指向:
int a = 3; // 这里改为一个普通变量
const int &ref = a; // 同样可行
令人疑惑的是,站在变量 a
自身的角度来看,它是可以更改的;但要是站在常量引用 ref
的角度来看,它却又是不可更改的。
实际上,变量 a
仍旧可以更改,不过是 ref
在“故弄玄虚”罢了。如果硬要以 ref
修改变量 a
,也是有办法的:
const_cast(ref) = 6; // “强行”修改 ref 引用的变量 a 储存的值为 6
在刚刚的例子中,const_cast
用于消除所给对象的不可变属性。比如 const int &
类型的对象,经过 const_cast
处理后会变为 int &
,也就是消除了 const
属性。const_cast
是 C++ 关键字之一,可以在程序的任意位置使用。
它的使用格式很简单:
const_cast(<欲消除不可变属性的对象>)
像先前提到的“故弄玄虚”地将可变对象约束为不可变对象的常量引用称作顶层 const;而从一开始就附带不可变属性的对象,包括指针与引用类型,称作底层 const。const_cast
只能正常处理顶层 const
,而处理底层 const
的结果会是未定义的。
C++ 还提供了一个与底层 const
性质相似的另一类 const
,它的关键字是constexpr
。与const
不同的是,constexpr
的值是在程序尚且还在编译时就定下来的,在运行时不可能被改变。由于它是编译时就确定的,因此它甚至不需要占用额外的内存来创建空间、存储自身,直接写死在编译后的可执行程序中。不过,它也仅适用于定义在编译时便能确定下来的值,而在程序运行时才能确定的值是不能以constexpr
定义的:
constexpr double pi = 3.1415926535; //很显然,constexpr 比 const 更适合表达数学常量
constexpr double pi_square = pi * pi; // 正确,参与初始化计算的对象均为 constexpr
constexpr double pi_half = pi / 2; // 正确,字面量(数字)同样也相当于 constexpr
本质上,先前所学的诸如数字、字符串这类字面量就带有constexpr
属性,而且理所应当--字面量本来就是在程序尚未编译时就确定下来的值。
由于带constexpr
属性的常量与字面量均可在编译时就确定其值,因此我们还可以用这些值做一些符合运算,定义新的constexpr
常量,就像刚刚的示例一样。
据此,我们也可以看到一个有趣的现象--C++ 编译器会在编译时完成尽可能多的常量计算,以此提升程序的运行性能,减少运行时不必要的计算:
std::cout << "4π = " << 4 * pi << std::endl
<< " π * π * π = " << pi * pi * pi << std::endl;
在上面的例子中,编译器很可能会在编译时就将4 * pi
与pi * pi * pi
的结果计算完成,然后直接"替换"掉原来的式子。这样一来,程序运行时就无需再大费周章地重算一遍,直接使用编译器提前算好的计算结果即可。
从字面量到变量,再从变量回到常量,无不是返璞归真的过程。善加利用常量,可以大幅提升程序的性能。
- 尝试口述下列程序的运行结果
std::cout << 1 + 2 + 3 + 4 << std::endl
constexpr int c = 3;
constexpr int cc = c * c + c;
std::cout << cc * c + cc << std::endl