本文根据众多互联网博客内容整理后形成,引用内容的版权归原始作者所有,仅限于学习研究使用。
C++11为了提高代码执行效率做了一些改善。这种改善之一就是: 生成常量表达式,允许程序利用编译时的计算能力。假如你熟悉模板元编程,你将发现constexpr使这一切变得更加简单。constexpr使我们很容易利用上编译时编程的优势。
常量表达式主要是允许一些计算发生在编译时,即发生在代码编译而不是运行的时候。这是很大的优化:假如有些事情可以在编译时做,它将只做一次,而不是每次程序运行时。需要计算一个编译时已知的常量,比如特定值的sine或cosin?确实你亦可以使用库函数sin或cos,但那样你必须花费运行时的开销。使用constexpr,你可以创建一个编译时的函数,它将为你计算出你需要的数值,用户的电脑将不需要做这些工作。
C++中的const可用于修饰变量、函数,且在不同的地方有着不同的含义,现总结如下。
C++中的const的目的是通过编译器来保证对象的常量性,强制编译器将所有可能违背const对象的常量性的操作都视为error。
对象的常量性可以分为两种:物理常量性(即每个bit都不可改变)和逻辑常量性(即对象的表现保持不变)。C++中采用的是物理常量性,例如下面的例子:
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的表现已经改变了!
逻辑常量性的另一个特点是,const对象中可以有某些用户不可见的域,改变它们不会违背逻辑常量性。Effective C++中的例子是:
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变量初始化时就提供给它初值:
const int i;
i = 5; // !error
const int j = 10; // ok
这个初值可以是编译时即确定的值,也可以是运行期才确定的值。如果给整数类型的const变量一个编译时初值,那么可以用这个变量作为声明数组时的长度:
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赋值的地方并没有进行替换。)
文件域的const变量默认是文件内可见的,如果需要在b.cpp中使用a.cpp中的const变量M,需要在M的初始化处增加extern:
//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 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语义不被破坏。
可以用const_cast来去掉某个指针或引用的const性质,或者用static_cast来为某个非const指针或引用加上const性质:
int i;
const int *cp = &i;
int *p = const_cast(cp);
const int *cp2 = static_cast(p); // here the static_cast is optional
C++类中的this指针就是一个自身为const的指针,而类的const方法中的this指针则是自身和指向都为const的指针。
类中的const成员变量可分为两种:非static常量和static常量。
类中的非static常量必须在构造函数的初始化列表中进行初始化,因为类中的非static成员是在进入构造函数的函数体之前就要构造完成的,而const常量在构造时就必须初始化,构造后的赋值会被编译器阻止。
class B {
public:
B(): name("aaa") {
name = "bbb"; // !error
}
private:
const std::string name;
};
static常量是在类中直接声明的,但要在类外进行唯一的定义和初始值,常用的方法是在对应的.cpp中包含类的static常量的定义:
// a.h
class A {
...
static const std::string name;
};
// a.cpp
const std::string A::name("aaa");
一个特例是,如果static常量的类型是内置的整数类型,如char、int、size_t等,那么可以在类中直接给出初始值,且不需要在类外再进行定义了。编译器会将这种static常量直接替换为相应的初始值,相当于宏替换。但如果在代码中我们像正常变量那样使用这个static常量,如取它的地址,而不是像宏一样只使用它的值,那么我们还是需要在类外给它提供一个定义,但不需要初始值了(因为在声明处已经有了)。
// 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
C++中可以用const去修饰一个类的非static成员函数,其语义是保证该函数所对应的对象本身的const性。
在const成员函数中,所有可能违背this指针const性(const成员函数中的this指针是一个双const指针)的操作都会被阻止,如对其它成员变量的赋值以及调用它们的非const方法、调用对象本身的非const方法。
但对一个声明为mutable的成员变量所做的任何操作都不会被阻止。这里保证了一定的逻辑常量性。
另外,const修饰函数时还会参与到函数的重载中,即通过const对象、const指针或引用调用方法时,优先调用const方法。
class A {
public:
int &operator[](int i) {
++cachedReadCount;
return data[i];
}
const int &operator[](int i) const {
++size; // !error
--size; // !error
++cachedReadCount; // ok
return data[i];
}
private:
int size;
mutable cachedReadCount;
std::vector 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
这个例子中,如果两个版本的operator[]有着基本相同的代码,可以考虑在其中一个函数中去调用另一个函数来实现代码的重用(参考Effective C++)。这里我们只能用非const版本去调用const版本。
int &A::operator[](int i) {
return const_cast(static_cast(*this).operator[](i));
}
其中为了避免调用自身导致死循环,首先要将*this转型为const A &,可以使用static_cast来完成。而在获取到const operator[]的返回值后,还要手动去掉它的const,可以使用const_cast来完成。一般来说const_cast是不推荐使用的,但这里我们明确知道我们处理的对象其实是非const的,那么这里使用const_cast就是安全的。
为了使函数获取编译时计算的能力,你必须指定constexpr关键字到这个函数。
constexpr int multiply (int x, int y)
{
return x * y;
}
// 将在编译时计算
const int val = multiply( 10, 10 );
除了编译时计算的性能优化,constexpr的另外一个优势是,它允许函数被应用在以前调用宏的所有场合。
例如,你想要一个计算数组size的函数,size是10的倍数。如果不用constexpr,你需要创建一个宏或者使用模板,因为你不能用函数的返回值去声明数组的大小。
但是用constexpr,你就可以调用一个constexpr函数去声明一个数组。
constexpr int getDefaultArraySize (int multiplier)
{
return 10 * multiplier;
}
int my_array[ getDefaultArraySize( 3 ) ];
int a = 4; //非常量表达式
getDefaultArraySize(a); //ok
constexpr修饰的函数,简单的来说,如果其传入的参数可以在编译时期计算出来,那么这个函数就会产生编译时期的值。但是,传入的参数如果不能在编译时期计算出来,那么constexpr修饰的函数就和普通函数一样了。不过,我们不必因此而写两个版本,所以如果函数体适用于constexpr函数的条件,可以尽量加上constexpr。
一个constexpr有一些必须遵循的严格要求:
注意:递归并不受限制,但只允许一个返回语句,那如何实现递归呢?可以使用三元运算符(?:)。例如,计算n的阶乘:
constexpr int factorial (int n) {
return n > 0 ? n * factorial( n - 1 ) : 1;
}
现在你可以使用factorial(2),编译器将在编译时计算这个值,这种方式运行更巧妙的计算,与内联截然不同。你无法内联一个递归函数。
constexpr修饰类的构造函数,即保证如果提供给该构造函数的参数都是constexpr,那么产生的对象中的所有成员都会是constexpr,该对象也就是constexpr对象了,可用于各种只能使用constexpr的场合。
注意,constexpr构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。
假如你有一个Circle类:
class Circle
{
public:
constexpr Circle (int x, int y, int radius) : _x( x ), _y( y ), _radius( radius ) {}
constexpr double getArea () {
return _radius * _radius * 3.1415926;
}
private:
int _x;
int _y;
int _radius;
};
将构造函数和getArea声明为constexpr,这样在编译期间,便能构造一个对象并能调用getArea函数获得area:
constexpr Circle c( 0, 0, 10 );
constexpr double area = c.getArea();
假如你将一个成员函数标记为constexpr,则顺带也将它标记为了const。如果你将一个变量标记为constexpr,则同样它是const的。但相反并不成立,一个const的变量或函数,并不是constexpr的。
constexpr:告诉编译器我可以是编译期间可知的,尽情的优化我吧。
const:告诉程序员没人动得了我,放心的把我传出去;或者放心的把变量交给我,我啥也不动就瞅瞅。
constexpr是一种比const 更严格的束缚, 它修饰的表达式本身在编译期间可知, 并且编译器会尽可能的 evaluate at compile time. 在constexpr 出现之前, 可以在编译期初始化的const都是implicit constexpr. 直到c++ 11, constexpr才从const中细分出来成为一个关键字。
而 const从1983年 c++ 刚改名的时候就存在了
如果你初学c++, 应当尽可能的, 合理的使用constexpr来帮助编译器优化代码。
https://www.xp.cn/b.php/72309.html
https://www.cnblogs.com/DswCnblog/p/6513310.html