C++ const用法详解(附带实例)
所谓常量,意味着不应该被修改(不变)的对象保持不变。作为 C++ 开发者,可以借助 const 关键字声明参数、变量和成员函数来保证这一点。
1) 在函数中不应该被修改的函数参数:
2) 不变的类数据成员:
3) 从外部看,不会修改对象状态的类成员函数:
4) 函数内的本地变量,此变量在其生命周期内都不变:
常量不是个人风格而是 C++开发过程中的核心原则。不幸的是,常量没有在书籍、C++社区和工作环境中获得足够的重视。经验法则是,所有不应该改变的都应该被声明为常量。这应该一直遵守,而不是在你需要清理重构代码等后期开发阶段才做。
当声明参数或变量为常量时,你要么将 const 关键字放在类型前(const T c),要么放在类型之后(T const c)。两者是等价的,不管你用哪种风格,都应该从右边开始理解声明:
当遇到指针时,这会有点复杂。下表展示了不同指针声明和它们的描述。
指向非常量对象的非常量指针 T*,可被隐式地转换为指向常量对象的非常量指针 T const *。然而,T** 不能隐式地转换为 T const **(跟 const T** 等价),这样做的话会导致常量对象可被指向非常量对象的指针所修改,如下例所示:
如果对象是常量,只有类的常量函数可被调用。然而,声明成员函数为常量不意味着此函数只能在常量对象上调用。从外部来看,这意味着此函数不会修改对象的内部状态。这是关键部分,但它经常被误解。有内部状态的类可通过公共接口暴露给它的用户。
然而,不是所有的内部状态都可被暴露,从公共接口层面可见的状态不一定有内部状态的直接表示(如果你对订单行进行建模,内部状态有物品数量和售卖价格,那么你可能有一个公共方法,通过将数量和价格相乘来暴露定单行金额)。因此,从对象公共接口可见的对象状态是一个逻辑状态。定义常量方法是一种声明,表示函数不会改变逻辑状态。然而,编译器阻止你通过此方法修改数据成员。为了避免这个问题,应该把会被常量函数修改的数据成员声明为 mutable。
在下面示例中,computation 是有 compute() 方法的类,执行长时间运行的计算操作。因为它不影响对象的逻辑状态,所以此函数被声明为常量。然而为了避免重复计算同一输入的结果,计算的结果存储在缓存中。为了能在常量函数中修改缓存,缓存被声明为 mutable:
以下类展示了类似的情况,实现了线程安全容器。共享内部数据的访问被 mutex 保护。类提供了例如加、减值的方法,也提供了 contains() 等方法,用来表明物品是否在容器中。因为此成员函数不修改对象逻辑状态,所以它被声明为常量。然而,共享内部状态必须被互斥量保护。为了对互斥量加锁、释放锁,可变操作(修改对象状态)和互斥量都必须声明为 mutable。
mutable 说明符允许我们修改类成员,即其对象被声明为 const。这跟 std::mutex 类型的 mt 成员情况相似,即使在声明为 const 的 contains() 方法中,也可被修改。
有时,方法或操作符有常量和非常量的重载版本。一般在下标操作符或提供对内部状态直接访问的方法中比较常见。这样做的原因是,此方法应该对常量和非常量对象都可用。然而,行为却不同:对于非常量对象,方法允许客户对访问的数据进行修改,但对于常量对象,则不然。因此,非常量下标操作符返回对非常量对象的引用,常量下标操作符返回对常量对象的引用:
需要注意的是,如果成员函数是常量,那么即使对象是常量,由此成员函数返回的数据也可能不是常量。
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<double, double> cache; 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 <typename T> class container { std::vector<T> data; mutable std::mutex mt; public: void add(T const & value) { std::lock_guard<std::mutex> lock(mt); data.push_back(value); } bool contains(T const & value) const { std::lock_guard<std::mutex> lock(mt); 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<contact> contacts; public: contact& operator[](size_t const index); contact const & operator[](size_t const index) const; };
需要注意的是,如果成员函数是常量,那么即使对象是常量,由此成员函数返回的数据也可能不是常量。