目录
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。
标签:index,const,int,mov,c++,constexpr,return From: https://www.cnblogs.com/violeshnv/p/16831724.html