C++总结:C++中的const和constexpr
C++中的const可用于修饰变量、函数,且在不同的地方有着不同的含义,现总结如下。
const的语义
C++中的const的目的是通过编译器来保证对象的常量性,强制编译器将所有可能违背const对象的常量性的操作都视为error。
struct A { int *ptr; }; int k = 5, r = 6; const A a = {&k}; a.ptr = &r; // !error *a.ptr = 7; // no error
a是const对象,则对a的任何成员进行赋值都会被视为error,但如果不改动ptr,而是改动ptr指向的对象,编译器就不会报错。这实际上违背了逻辑常量性,因为A的表现已经改变了!
class CTextBlock { public: ... std::size_t length()const; private: char *pText; std::size_t textLength;// last calculated length of textblock bool lengthIsValid;// whether length is currently valid };
CTextBlock对象每次调用length方法后,都会将当前的长度缓存到textLength成员中,而lengthIsValid对象则表示缓存的有效性。这个场景中textLength和lengthIsValid如果改变了,其实是不违背CTextBlock对象的逻辑常量性的,但因为改变了对象中的某些bit,就会被编译器阻止。C++中为了解决此问题,增加了mutable关键字。
本部分总结:C++中const的语义是保证物理常量性,但通过mutable关键字可以支持一部分的逻辑常量性。
const修饰变量
const int i; i = 5;// !error const int j = 10;// ok
const int COMPILE_CONST = 10; const int RunTimeConst = cin.get(); int a1[COMPLIE_CONST];// ok in C++ and error in C int a2[RunTimeConst];// !error in C++
因为C++编译器可以将数组长度中出现的编译时常量直接替换为其字面值,相当于自动的宏替换。(gcc验证发现,只有数组长度那里直接做了替换,而其它用COMPILE_CONST赋值的地方并没有进行替换。)
//a.cpp extern const int M = 20; //b.cpp extern const int M;
一般认为将变量的定义放在.h文件中会导致所有include该.h文件的.cpp文件都有此变量的定义,在链接时会造成冲突。但将const变量的定义放在.h文件中是可以的,编译器会将这个变量放入每个.cpp文件的匿名namespace中,因而属于是不同变量,不会造成链接冲突。(注意:但如果头文件中的const量的初始值依赖于某个函数,而每次调用此函数的返回值不固定的话,会导致不同的编译单元中看到的该const量的值不相等。猜测:此时将该const量作为某个类的static成员可能会解决此问题。)
const修饰指针与引用
const修饰引用时,其意义与修饰变量相同。但const在修饰指针时,规则就有些复杂了。
const int *p1;// p1 is a non-const pointer and points to a const int int *const p2;// p2 is a const pointer and points to a non-const int const int *const p3;// p3 is a const pointer and points to a const it const int *pa1[10];// pa1 is an array and contains 10 non-const pointer point to a const int int *const pa2[10];// pa2 is an array and contains 10 const pointer point to a non-const int const int (* p4)[10];// p4 is a non-const pointer and points to an array contains 10 const int const int (*pf)();// pf is a non-const pointer and points to a function which has no arguments and returns a const int ...
const指针的解读规则差不多就是这些了……
指针自身为const表示不可对该指针进行赋值,而指向物为const则表示不可对其指向进行赋值。因此可以将引用看成是一个自身为const的指针,而const引用则是const Type * const指针。
指向为const的指针是不可以赋值给指向为非const的指针,const引用也不可以赋值给非const引用,但反过来就没有问题了,这也是为了保证const语义不被破坏。
int i; const int *cp = &i; int *p =const_cast<int *>(cp); const int *cp2 =static_cast<const int *>(p);// here the static_cast is optional
C++类中的this指针就是一个自身为const的指针,而类的const方法中的this指针则是自身和指向都为const的指针。
类中的const成员变量
类中的const成员变量可分为两种:非static常量和static常量。
非static常量:
class B { public: B(): name("aaa") { name ="bbb";// !error } private: const std::string name; };
static常量:
// a.h class A { ... static const std::string name; }; // a.cpp const std::string A::name("aaa");
// a.h class A { ... static const int SIZE = 50; }; // a.cpp const int A::SIZE = 50;// if use SIZE as a variable, not a macro
const修饰函数
C++中可以用const去修饰一个类的非static成员函数,其语义是保证该函数所对应的对象本身的const性。在const成员函数中,所有可能违背this指针const性(const成员函数中的this指针是一个双const指针)的操作都会被阻止,如对其它成员变量的赋值以及调用它们的非const方法、调用对象本身的非const方法。但对一个声明为mutable的成员变量所做的任何操作都不会被阻止。这里保证了一定的逻辑常量性。
class A { public: int &operator[](int i) { ++***dReadCount; return data[i]; } const int &operator[](int i)const { ++size;// !error --size;// !error ++***dReadCount;// ok return data[i]; } private: int size; mutable ***dReadCount; std::vector<int> data; }; A &a = ...; const A &ca = ...; int i = a[0];// call operator[] int j = ca[0];// call const operator[] a[0] = 2;// ok ca[0] = 2;// !error
int &A::operator[](int i) { return const_cast<int &>(static_cast<const A &>(*this).operator[](i)); }
其中为了避免调用自身导致死循环,首先要将*this转型为const A &,可以使用static_cast来完成。而在获取到const operator[]的返回值后,还要手动去掉它的const,可以使用const_cast来完成。一般来说const_cast是不推荐使用的,但这里我们明确知道我们处理的对象其实是非const的,那么这里使用const_cast就是安全的。
constexpr
constexpr是C++11中新增的关键字,其语义是“常量表达式”,也就是在编译期可求值的表达式。最基础的常量表达式就是字面值或全局变量/函数的地址或sizeof等关键字返回的结果,而其它常量表达式都是由基础表达式通过各种确定的运算得到的。constexpr值可用于enum、switch、数组长度等场合。
constexpr int Inc(int i) { return i + 1; } constexpr int a = Inc(1);// ok constexpr int b = Inc(cin.get());// !error constexpr int c = a * 2 + 1;// ok
struct A { constexpr A(int xx,int yy): x(xx), y(yy) {} int x, y; }; constexpr A a(1, 2); enum {SIZE_X = a.x, SIZE_Y = a.y}; constexpr的好处:
- 是一种很强的约束,更好地保证程序的正确语义不被破坏。
- 编译器可以在编译期对constexpr的代码进行非常大的优化,比如将用到的constexpr表达式都直接替换成最终结果等。
- 相比宏来说,没有额外的开销,但更安全可靠。