“const 的一件奇妙的事情是, 它允许你指定一个语义约束(也就是指定一个
不能被改动的对象
), 而编译器会强制实施这项措施.”
const 多才多艺, 可以用于修饰类外的常量, 声明为 static 的对象, 或者修饰类内的成员. 你可以用 const 修饰指针本身, 或者修饰指针所指的事物, 使其不能被改变, 或者都不.
char* p
指针本身和所指数据都不为 constconst char*p
指针本身不为 const, 但是所指数据为 constchar* const p
指针本身为 const, 但是所指数据不为 constconst char* const p
指针本身和所指数据都为 constchar const * p
指针本身不为const, 但是所指数据为const
const最具威力的用法是和函数声明结合, 在一个函数声明中可以和函数返回值, 各参数值, 函数自身(如果是成员函数的话)产生关联.
让函数返回一个const, 往往可以降低用户错误操作的概率, 如以下代码:
class data
{
public:
data operator*(const data& a)
{
}
};
int main()
{
data a, b, c;
if (a * b = c)
{
}
return 0;
}
虽然不知道为什么要对乘法进行赋值, 可能只是单纯的少打了个等号, 如果加上 const 返回就会在编译阶段报错了.
使用 const 成员函数有两个必要性: 第一, 他使得接口函数很容易被理解, 很容易就可以知道哪个函数可以改动成员内容而哪些函数不可以改变. 第二, 他使得"操作 const 对象"成为可能. 这对于编写高效代码是非常关键的.
有一个很多人没有注意到的 C++ 特性, 那就是如果两个成员函数只是常量性不同, 他们是可以构成重载的. 同时我们需要注意, 如果一个函数不返回引用, 那么对返回值的修改是不合法的, 我觉得这是一个基本常识.
有意思的是, 对于判断成员函数被 const 修饰意味着什么, 有两种流行的概念: 1. bitwise constness(又叫做 physical constness), 2. logical constness.
bitwise constness 阵营相信, const 修饰的成员函数不会修改类的任何一个 bit. 这样的好处是, 你可以很好地去侦测违反点: 编译器只要寻找成员变量的赋值动作就可以. bitwise constness 正是 C++ 对常量性的定义, 因此, const 成员函数不可以更改类内的任何 not-static 成员变量.
但是, 很遗憾, 许多成员函数不具备 const 的性质, 但是仍然可以通过 bitwise 检测, 比如下面这个例子:
class data
{
public:
data()
:data_(nullptr), size_(0)
{
data_ = new char[6]{ 97,98,99,100,101,102 };
size_ = 0;
}
~data()
{
if (data_ != nullptr)
{
delete[] data_;
}
}
char& get(const size_t pos) const
{
assert(pos >= size_);
return data_[pos];
}
private:
char* data_;
size_t size_;
};
int main()
{
const data d;
d.get(0) = 122;
return 0;
}
在这个例子里, 你就会发现, 虽然你没有对类做出明面上的修改, 因为类就两个成员, 你都没有修改, 但是你实际上通过引用修改了我们认为他存放的实际值. 也就是修改了我们逻辑上认为的你不应该修改的值.
解决方法可以是使用一个标记位判断是否有效, 但是这种有效也是会在 const 接口函数中改变的, 所以并不可以被坚持要 bitwise constness 的编译器同意. 这时可以利用 C++ 的一个和 const 相关的摆动场 mutable, mutable 会告诉编译器这个成员是 const 但是, 他还是有可能会被改动, 所以编译器会进行放行.
在 const 函数内部, 我们要尽量避免 const 和 non-const 函数中的重复, 如下面这段代码:
class test
{
public:
const char& operator[] (const size_t position) const
{
// 边界检查
// 日志数据访问
// 检验数据完整性
return text[position];
}
char& operator[] (const size_t position)
{
// 边界检查
// 日志数据访问
// 检验数据完整性
return text[position];
}
private:
std::string text;
};
可以看到, 上下两个函数有一大部分重复了, 或许你会想到利用相同的接口来避免这种重复, 但是这样仍没有解决本质问题, 就是能不能让两者之间实现套用.
class test
{
public:
const char& operator[] (const size_t position) const
{
// 边界检查
// 日志数据访问
// 检验数据完整性
return text[position];
}
char& operator[] (const size_t position)
{
return const_cast<char &>(static_cast<const test &>(*this)[position]);
}
private:
std::string text;
};
const_cast是一种C++运算符,主要是用来去除复合类型中const和volatile属性(没有真正去除).
static_cast是一种C++运算符, 主要用于为某个值加上 const 属性.
这里的意思很明显了, 先将 this 转化为 const 指针, 然后调用 const 的接口, 接着将返回的结果去掉 const 标签, 进行返回, 从而达到复用的目的.
你或许会有疑问, 那为什么不是让 const 去调用 non-const 函数呢? 这样想是不对的, const 接口 与 non-const 接口的区别在于, const 接口承诺不会更改内容, 如果让 const 去调用 non-const 其实就类似于我们的权限放大了, 把一个 const 的引用给了一个 非 const 的引用.
<Effective C++> 把 const 放在最前面两个条款来重点谈, 一是说明 const 是一个非常强大的武器, 二是说明部分程序员可能无法精准控制 const 的力道, 或者说打偏.
重点:
- 某些变量使用 const 可以让编译器为你检查出一些不该有的错误.
- 编译器强制使用 bitwise constness, 但实际编写应该采取 logic constness.
- 当 const 和 non-const 接口有大部分的实际的等价时, 你应该让 non-const 调用 const 版本避免代码重复.