c++ 中 const, constexpr 的使用
C++ 与 C 语言相比有着更强的类型检查,包括四种 cast
,左值右值之分,reference
,以及最重要的——对 const
的要求。
const
是一个相当麻烦的要求,比如其强大的“传播性”——只要在一个地方使用,就可能蔓延到各个角落,出现各种编译错误。但编程实践证明 const
的使用是值得的(甚至 Rust 语言已经将 const
作为基本要求了),可以提高代码的安全性和可理解性。
这种要求会体现在很多方面。
char a[] = "Hello"; char *p = a; const char *q = a; char *const r = a; const char *const s = a;
你能说出它们有什么区别吗? (顶层 const
和底层 const
)
同时在函数的参数、返回值中也要考虑 const
参数
bool longerThan1(const string &s1, const string &s2) { return s1 > s2; } bool longerThan2(string &s1, string &s2) { return s1 > s2; }
string a = "Hello"; longerThan1(a, "World"); // Yes longerThan2(a, "World"); // No
不加 const
然编译器认为你想要对参数做出某些改变,而 "World"
可不是一个真正的 string
对象,自然不能让你改变它。
在这里有人认为可以使用下列函数
bool longerThan3(string s1, string s2) { return s1 > s2; } longerThan3(a, "World"); // ?
然而这是欠缺考虑的形式
- 对于
a
来说,longerThan3()
对a
完完全全地复制了一遍,而longerThan1()
使用的const reference
实质是一种指针,不会复制a
。longerThan3()
花费了无用的时间和空间。 - 对于
"World"
来说,longerThan1(), longerThan3()
都使用"World"
创建了一个新的string
对象,在这一点上是一样的;但longerThan1()
创建的s2
是带有const
的,这防止了对s2
无谓的修改。
因此在任何时候,能用 const
就用 const
。
还要考虑最特别的参数 this
,最好的实践同样是是:不改变成员的函数全都使用 const this
。同时,对某些函数可以使用重载,比如
class Array { public: const int &operator[](size_t index) const { return a[index]; } int &operator[](size_t index) { return a[index]; } private: int *a; }; void printArray(const Array &a) { for (size_t i = 0, x = a.size(); i != x; ++i) cout << a[i]; }
const Array
和 Array
可以使用有 const this
的函数,这点在传递 const Array &a
作为其他某个函数的参数的时候使用。
例外
不过如果真的遇到了需要用到用值传递的函数参数,则应该避免使用 const
,例如
void foo(int x); // Yes void foo(const int x); // No
因为这属于多此一举,用值传递的参数只有函数内可以看见和使用,不会影响到原来的那个变量。这属于限制了函数的创建者,却没影响到函数的调用者。整个函数的逻辑都应该被创建者掌握,又何必多加限制呢。
返回值
对于返回值也是这样,只要稍加考虑就能明白,在绝大多数情况我们的返回值都应该是常量。因为返回值通常被赋值给另一个对象,亦或是弃之不用。考虑如下函数:
class B { public: B(int bb) : b(bb) {} B() : B(0) {} friend B operator+(const B &lhs, const B &rhs) { return B(lhs.b + rhs.b); } // 只是在类里面实现friend函数罢了 private: int b; }; result = B(3) + B(5);
看上去,result
是 operator+()
的返回值,然而实际情况却是这样
__t = B(3) + B(5); result = __t;
__t
才是返回值,然后被赋值给 result
。因此完全有可能出现这种情况。
B(3) + B(5) = 5;
虽然做法比较离奇,但谁知道调用者会怎么做呢(也有可能是 ==
打成了 =
),还是使用 const
为返回值加上限制吧。 friend const B operator+(const B &lhs, const B &rhs) { return B(lhs.b + rhs.b); }
例外
考虑 STL 的实践
vector<int> v{1, 2, 3}; for (auto p = ++v.begin(), q = --v.end(); p != q; ++p) cout << *p;
这里的 ++ --
就说明没有为 begin(), end()
的返回值加上 const
,这是为了方便使用者调整。
const this
和成员
值得注意的是,对于上述的 class Array
,下面这样的函数是可行的。
int &operator[](size_t index) const { return a[i]; }
这是因为,const this
所代表的是:这个类成员不能变动,而 a
(指针)是 Array
的成员,但 a[index]
(指针所指的位置)却不是 Array
的成员
至于此时 const
该不该加则需要一些考虑(在 bitwise constness
和 logical constness
之间)。对于上面的这种情况,则应该还是要加的,因为这里 Array
是数组类。虽然编译器不认为 a[index]
是成员,但从逻辑上讲,考虑到我们实现 Array
的目的——作为一个容器类,a[index]
是成员。
而在另外的一些情况则可以不加。不仅如此,C++ 还提供了一个关键字 mutable
让你去掉 const
限制,也就是说,即使在是成员的情况下也可以修改某些量。加或不加,这是一个问题。
const_cast
const_cast
最大的作用就是在加或不加 const
的重载函数间提供方便。
比如 class Array
中的 operator[]()
可以改写成
const int &operator[](size_t index) const { return a[index]; } int &operator[](size_t index) { return const_cast<int &>(const_cast<const A &>(*this)[index]); }
比起原来的是不是还要麻烦许多?但是你看看这个
const int &operator[](size_t index) const { if (index < 0 || index >= size()) throw std::invalid_argument("index out of bound"); #ifdef DEBUG // recording #endif // DEBUG return a[index]; } int &operator[](size_t index) { return const_cast<int &>(const_cast<const A &>(*this)[index]); }
就不用写两遍了,减少了很多麻烦。必须指出的是,只可能在 non-const
函数中用这种方法调用 const
函数,反之则不行。
与 constexpr
的关系
实际上两者没有什么关系,对于 constexpr
,我称为真正的常量。在编译器就可以展开,可以用在 static_assert
中。用法有三种
- 变量
- 函数
- 构造函数
函数
对函数使用后,能在编译期就可以直接算出值,但也有要求
- 所有相关变量、函数都在编译期可以求得。这是总要求,其他要求都是衍生。
- 参数是常量;
- 除了
using, typedef, static_assert
只能有return
语句; - 调用的函数也得是
constexpr
; - 不能使用全局变量等可能在运行期改变的量;
- 返回常量。
constexpr int add(int x, int y) { return x + y; } int main() { int k = add(3, 5); // 常量版本 int t = add(k, 2); // 非常量版本 }
看看反汇编,可以被理解为常量的就直接就算出来了
0x000000000000073a <+0>: push %rbp 0x000000000000073b <+1>: mov %rsp,%rbp 0x000000000000073e <+4>: sub $0x10,%rsp 0x0000000000000742 <+8>: movl $0x8,-0x8(%rbp) ; k 在这 0x0000000000000749 <+15>: mov -0x8(%rbp),%eax 0x000000000000074c <+18>: mov $0x2,%esi 0x0000000000000751 <+23>: mov %eax,%edi 0x0000000000000753 <+25>: callq 0x7c0 <_Z3addii> ; 只调用一次 0x0000000000000758 <+30>: mov %eax,-0x4(%rbp) ; t 在这 0x000000000000075b <+33>: mov $0x0,%eax 0x0000000000000760 <+38>: leaveq 0x0000000000000761 <+39>: retq
要注意的是,定义要放在调用前,像下面这样就不能识别了
constexpr int add(int x, int y); int main() { int k = add(3, 5); int t = add(k, 2); } constexpr int add(int x, int y) { return x + y; }
0x000000000000073a <+0>: push %rbp 0x000000000000073b <+1>: mov %rsp,%rbp 0x000000000000073e <+4>: sub $0x10,%rsp 0x0000000000000742 <+8>: mov $0x5,%esi 0x0000000000000747 <+13>: mov $0x3,%edi 0x000000000000074c <+18>: callq 0x7cb <_Z3addii> 0x0000000000000751 <+23>: mov %eax,-0x8(%rbp) ; k 0x0000000000000754 <+26>: mov -0x8(%rbp),%eax 0x0000000000000757 <+29>: mov $0x2,%esi 0x000000000000075c <+34>: mov %eax,%edi 0x000000000000075e <+36>: callq 0x7cb <_Z3addii> 0x0000000000000763 <+41>: mov %eax,-0x4(%rbp) ; t 0x0000000000000766 <+44>: mov $0x0,%eax 0x000000000000076b <+49>: leaveq 0x000000000000076c <+50>: retq
变量
对变量使用,变成常量,没什么好说的,主要注意在类中的情况
class B { constexpr int a = 10; // Wrong: need to be static static constexpr int b = 10; // Right static constexpr double c = 10; // Right static const int d = 10; // Right static const double e = 10; // Wrong: non-integral type static const double f; // Right }; const B::f = 10;
使用 constexpr
可以直接在类中初始化 double
常量
构造函数
可以像内置类型一样定义常量,其他要求大致和函数一样。
class B { public: constexpr B(int bb) : b(bb) {} private: int b; }; int foo() { constexpr B b(55); }
C++17、C++20 还有更多用法,不过就不讲了,现在用的最多的还是 C++14。
本文作者:violeshnv
本文链接:https://www.cnblogs.com/violeshnv/p/16831724.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步