Chapter 23: Constants ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 常量是被赋值后其值不会发生变化的变量。这允许编译器确保变量的值在代码中的任何位置不会被错误修改。 常量变量 ================= 可以类型前或后添加const关键字从而使得变量成为常量。此修饰符意味着变量变为只读,因而必须在声明的同时赋值。在其它位置尝试修改值会导致编译时错误。 .. code:: const int var = 5; int const var2 = 10; // alternative order 常量指针 ============== 当涉及到指针时,可以通过两种方式使用const。首先,指针可以为常量,这意味着它不能被修改来指向其它的位置。 .. code:: int myPointee; int* const p = &myPointee; // pointer constant 其次,指向的内容可以声明为常量。这意味着不能通过指针修改所指向的变量。 .. code:: const int* q = &myPointee; // pointee constant 也可以将指针和指向的内容同时声明为常量,从而使其均变为可读。从右至左读取类型会使其更易于理解,所以在此情况下,r是一个指向常量int的常量指针。 .. code:: const int* const r = &myPointee; // pointer & pointee constant 注意,常量变量也不可以通过常量指针进行指向。这可以避免程序员错误地使用指针重写常量变量。 .. code:: const int myConst = 3; int* s = &myConst; // error: const to non-const assignment 常量引用 ============ 引用可以像指针一样声明为常量。然而,因为重新设置引用是不被允许的,因而将引用声明为const是多余的。它仅用于保护引用被修改。 .. code:: const int& y = myPointee; // referee constant 常量对象 =============== 类似于变量,指针与引用,对象也可以声明为常量。以下面的类为例。 .. code:: class MyClass { public: int x; void setX(int a) { x = a; } }; 此类的常量对象不能被重新赋值为其它实例。对象的常量化也会影响其所包含的域,防止它们被修改。 .. code:: const MyClass a, b; a = b; // error: object is const a.x = 10; // error: object field is const 常量方法 ============ 由于最后一条限制,非常量的方法不能在常量对象上调用,因为这样的方法被允许修改对象的域。 .. code:: a.setX(2); // error: cannot call non-const method 它们仅可以调用常量方法,这些方法是在方法体前以const修饰符为标识。 .. code:: int getX() const { return x; } // constant method const修饰符意味着方法不允许修改对象的状态,因而可以由此类的常量对象中进行安全地调用。更特别的,const修饰符应用于隐式地传递给方法的this指针。这可以有效地限制方法修改对象的域或是在类中调用任何非常量方法。 常量返回类型与参数 ===================== 除了使得方法常量化以外,返回类型与方法参数也可以成为只读。例如,由常量方法中按引用而不是按值返回一个域,将其作为常量返回是很重要的,以维护对象的常量化。并非所有的C++编译器都能够捕获这种误用。 .. code:: const int& getX() const { return x; } 对象应总是通过const引用向函数与方法传递或由函数或方法中返回。这会改善性能,因为它会避免不必要的拷贝。 常量域 ================== 类中的静态与实例域都可以声明为常量。常量实例域必须使用类中初始化器或是构造器初始化列表为其赋值。 .. code:: class MyClass { public: int a; const int b; const int c = 3; MyClass() : a(1), b(2) {} }; 常量静态域必须在类声明之外定义,使用与非常量静态域相同的方式。一个例外是当常量静态域为内联或整型数据类型时。这类域要在声明的同时初始化。 .. code:: class MyClass { public: const static double c1; const inline static double c2 = 1.23; const static int c3 = 5; }; const double MyClass::c1 = 1.23; 常量表达式 ================= 关键字constexpr被引入到C++11中来表明一个常量表达式。类似于const,它可以被应用于变量使其变为常量,如果有代码尝试修改值则会导致编译错误。 .. code:: constexpr int myConst = 5; myConst = 3; // error: variable is const 没于const变量在运行时赋值,常量表达式变量总是在编译时计算。因而这样的变量可以用于需要编译时常量的任何时刻,例如数据或枚举声明中。在C++11以前,这仅允许整数或枚举类型。 .. code:: int myArray[myConst + 1]; // allowed 函数构造器也可以定义为常量表达式,而不允许使用const。在函数上使用constexpr限定了允许函数做什么。简单来说,函数仅能引用其它constexpr函数与全局constexpr变量。 .. code:: constexpr int getDefaultSize(int multiplier) { return 3 * multiplier; } constexpr函数的返回值仅在其参数为常量表达式时可以确保在编译时计算,而返回值用于需要编译时常量的位置。 .. code:: // Compile-time evaluation int myArray[getDefaultSize(10)]; 如果函数未使用常量参数进行调用,它则会像普通函数一样在运行时返回值。 .. code:: // Runtime evaluation int mul = 10; int size = getDefaultSize(mul); 在C++17中,如果lambda表达满足constexpr函数的条件,则它为隐式constexpr。所以lambda也可以用于编译时环境。 .. code:: auto answer = [](int i) { return 10+i; }; constexpr int reply = answer(32); // "42" 构造器也可以使用constexpr声明,来构造常量表达式对象。这样的构造器必须非常简单。 .. code:: class Circle { public: int r; constexpr Circle(int x) : r(x) {} }; 当使用常量表达式参数调用时,结果将是具有只读域的编译时生成对象。对于其它参数,其行为则类似于普通构造器。 .. code:: // Compile-time object constexpr Circle c1(5); // Runtime object int x = 5; Circle c2(x); C++17添加了constexpr的另一种使用:在编译时计算条件语句的能力。此特性允许基于常量条件,在编译时丢弃if语句分支,从而减少编译时间与编译文件的尺寸。 .. code:: constexpr int debug = 0; if constexpr(debug) { // Discarded if condition is false } 在C++17之前,虚函数不能被定义为constexpr。C++20中放松了此限制,允许虚函数使用常量表达式进行调用。注意,一个constexpr虚函数会覆盖非constexpr虚函数,如下面示例所示。 .. code:: struct Parent { virtual int num() const = 0; }; struct Child: public Parent { constexpr virtual int num() const { return 3; } }; constexpr Child c; static_assert( c.num() == 3, "num is not 3" ); 这里所看到的static_assert声明被用于在编译时断言。如果断言失败,即条件计算为假时发生,编译器中止编译并显示错误消息。 立即函数 ================ 正如前面所提到的,constexpr函数的返回值并不是总在编译时计算。为此目的,C++20引入了立即函数。立即函数是使用consteval关键字定义的,经表明函数总是返回编译时常量。这样的函数可以用于需要常量表达式的环境中,如下面的示例所示。 .. code:: consteval int doubleIt(int i) { return 2*i; } constexpr int a = doubleIt(10); // ok int x = 10; int b = doubleIt(x); // error: call does not produce a constant 常量规则 ================= 通常来说,如果变量不需要被修改,总是将其声明为常量会是一个好主意。这可以确保在程序中的其它位置不会被错误修改,从而有助于避免bug。通过为编译器提供将常量表达式硬编码到编译程序中,也可以得到性能提升。这可以允许表达式只被计算一次-在编译时-而不是在每次程序运行时计算。