首页 > 编程笔记 > C++笔记 阅读:56

C++ const用法详解(附带实例)

所谓常量,意味着不应该被修改(不变)的对象保持不变。作为 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)。两者是等价的,不管你用哪种风格,都应该从右边开始理解声明:
当遇到指针时,这会有点复杂。下表展示了不同指针声明和它们的描述。

表达式 描述
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 & cT 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;
};

需要注意的是,如果成员函数是常量,那么即使对象是常量,由此成员函数返回的数据也可能不是常量。

相关文章