所谓常量,意味着不应该被修改(不变)的对象保持不变。作为 C++ 开发者,可以借助 const 关键字声明参数、变量和成员函数来保证这一点。
C++ const使用方式
为了保证程序的常量正确性,你通常应该声明以下为常量:
1) 在函数中不应该被修改的函数参数:
struct session {};
session connect(std::string const & uri, int const timeout = 2000)
{
/* do something */
return session { /* ... */ };
}
2) 不变的类数据成员:
class user_settings
{
public:
int const min_update_interval = 15;
/* other members */
};
3) 从外部看,不会修改对象状态的类成员函数:
class user_settings
{
bool show_online;
public:
bool can_show_online() const {return show_online;}
/* other members */
};
4) 函数内的本地变量,此变量在其生命周期内都不变:
user_settings get_user_settings()
{
return user_settings {};
}
void update()
{
user_settings const us = get_user_settings();
if(us.can_show_online()) { /* do something */ }
/* do more */
}
深度剖析C++ const
在 C++ 中,对象和成员函数声明为常量有几个重要的好处:
可以避免对对象的意外更改和故意更改,这些更改在某些情况下会导致不正确的程序行为;
使编译器可以执行更好的性能优化;
对其他用户而言,代码语义文档化。
常量不是个人风格而是 C++开发过程中的核心原则。不幸的是,常量没有在书籍、C++社区和工作环境中获得足够的重视。经验法则是,所有不应该改变的都应该被声明为常量。这应该一直遵守,而不是在你需要清理重构代码等后期开发阶段才做。
当声明参数或变量为常量时,你要么将 const 关键字放在类型前(const T c),要么放在类型之后(T const c)。两者是等价的,不管你用哪种风格,都应该从右边开始理解声明:
const T c 可理解为 c 是一个 T,T 为常量;
T const c 则表示 c 是一个常量 T。
当遇到指针时,这会有点复杂。下表展示了不同指针声明和它们的描述。
表达式
描述
T* p
p 是非常量 T 的非常量指针
const T* p
p 是 T 的非常量指针,T 为常量
T const * p
p 是常量 T 的非常量指针(跟前一项一样)
const T * const p
p 是 T 的常量指针,T 为常量
T const * const p
p 是常量 T 的常量指针(跟前一项一样)
T** p
p 是指向非常量 T 的非常量指针的非常量指针
const T** p
p 是指向常量 T 的非常量指针的非常量指针
T. const ** p
和 const T** p 一样
const T* const * p
p 是指向常量指针的非常量指针,其中常量指针指向常量 T
T const * const * p
和 const T* const * p 一样
const 关键字放于类型后更自然,因为它和从右到左的阅读方向一致。
对于引用,情况是类似的:const T & c 和 T const & c 是等价的,即 c 是指向常量 T 的引用。然而,T const & const c 表示的 c 是指向常量 T 的常量引用,这没有意义,因为引用(变量的别名)是隐式常量,它们无法被修改为另一个变量的别名。
指向非常量对象的非常量指针 T*,可被隐式地转换为指向常量对象的非常量指针 T const *。然而,T** 不能隐式地转换为 T const **(跟 const T** 等价),这样做的话会导致常量对象可被指向非常量对象的指针所修改,如下例所示:
int const c = 42;
int* x;
int const ** p = &x; // this is an actual error
*p = &c;
*x = 0; // this modifies c
如果对象是常量,只有类的常量函数可被调用。然而,声明成员函数为常量不意味着此函数只能在常量对象上调用。从外部来看,这意味着此函数不会修改对象的内部状态。这是关键部分,但它经常被误解。有内部状态的类可通过公共接口暴露给它的用户。
然而,不是所有的内部状态都可被暴露,从公共接口层面可见的状态不一定有内部状态的直接表示(如果你对订单行进行建模,内部状态有物品数量和售卖价格,那么你可能有一个公共方法,通过将数量和价格相乘来暴露定单行金额)。因此,从对象公共接口可见的对象状态是一个逻辑状态。定义常量方法是一种声明,表示函数不会改变逻辑状态。然而,编译器阻止你通过此方法修改数据成员。为了避免这个问题,应该把会被常量函数修改的数据成员声明为 mutable。
在下面示例中,computation 是有 compute() 方法的类,执行长时间运行的计算操作。因为它不影响对象的逻辑状态,所以此函数被声明为常量。然而为了避免重复计算同一输入的结果,计算的结果存储在缓存中。为了能在常量函数中修改缓存,缓存被声明为 mutable:
class computation
{
double compute_value(double const input) const
{
/* Long running operation */
return input;
}
mutable std::map
public:
double compute(double const input) const
{
auto it = cache.find(input);
if(it != cache.end()) return it->second;
auto result = compute_value(input);
cache[input] = result;
return result;
}
};
以下类展示了类似的情况,实现了线程安全容器。共享内部数据的访问被 mutex 保护。类提供了例如加、减值的方法,也提供了 contains() 等方法,用来表明物品是否在容器中。因为此成员函数不修改对象逻辑状态,所以它被声明为常量。然而,共享内部状态必须被互斥量保护。为了对互斥量加锁、释放锁,可变操作(修改对象状态)和互斥量都必须声明为 mutable。
template
class container
{
std::vector
mutable std::mutex mt;
public:
void add(T const & value)
{
std::lock_guard
data.push_back(value);
}
bool contains(T const & value) const
{
std::lock_guard
return std::find(std::begin(data), std::end(data), value)
!= std::end(data);
}
};
mutable 说明符允许我们修改类成员,即其对象被声明为 const。这跟 std::mutex 类型的 mt 成员情况相似,即使在声明为 const 的 contains() 方法中,也可被修改。
有时,方法或操作符有常量和非常量的重载版本。一般在下标操作符或提供对内部状态直接访问的方法中比较常见。这样做的原因是,此方法应该对常量和非常量对象都可用。然而,行为却不同:对于非常量对象,方法允许客户对访问的数据进行修改,但对于常量对象,则不然。因此,非常量下标操作符返回对非常量对象的引用,常量下标操作符返回对常量对象的引用:
class contact {};
class addressbook
{
std::vector
public:
contact& operator[](size_t const index);
contact const & operator[](size_t const index) const;
};
需要注意的是,如果成员函数是常量,那么即使对象是常量,由此成员函数返回的数据也可能不是常量。