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

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)。两者是等价的,不管你用哪种风格,都应该从右边开始理解声明:

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 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

class container

{

std::vector data;

mutable std::mutex mt;

public:

void add(T const & value)

{

std::lock_guard lock(mt);

data.push_back(value);

}

bool contains(T const & value) const

{

std::lock_guard 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 contacts;

public:

contact& operator[](size_t const index);

contact const & operator[](size_t const index) const;

};

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

相关推荐