Skip to content

Latest commit

 

History

History
143 lines (94 loc) · 6.67 KB

2-3.md

File metadata and controls

143 lines (94 loc) · 6.67 KB

固化态 - 常量限定符

世间有许多亘古不变的真理,人类不得撼动亦无法改变它的存在。不断地认识与利用真理,恰是人类文明不断进步的表现。

而在 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;而从一开始就附带不可变属性的对象,包括指针与引用类型,称作底层 constconst_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 * pipi * 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