Effective C++ 学习笔记——条款03:尽可能使用const
const的功能:
- 用在class外部修饰global或namespace作用于中的常量;
- 修饰文件、函数、块区域作用域中被声明为static的对象;
- 用于修饰class内部的static和non-static成员变量;
- 用于指出指针自身、指针所指物,或两者都(不)是const。
const与指针:
-
const出现在 * 左边,表示被指物为常量;
-
const出现在 * 右边,表示指针自身是常量;
-
如果 * 两边均有const,表示被指物和指针两者都是常量。
例如:const char* p; //数据是const,数据不允许被改变 char* const p; //指针是const,指针不允许被改变 const char* const p; //数据与指针都是const,数据与指针都不可以被改变
记忆方法:
- const 在 * 左侧,称常量指针,即指向常量的指针——const修饰数据;
- 在 const 左侧,称指针常量,即指针是常量——const修饰指针。
- 注意,此处指的是 const 与 * 的相对位置,以下两种情况完全相同:
const char* pw; //都表示指向常量char的指针 char const* pw;
const与迭代器:
迭代器:STL中的迭代器是以指针为基础塑造出来的,所以迭代器的作用相当于一个 T* 指针。
- 声明迭代器为 const 就相当于声明指针为 const,即 T* const 指针,表明迭代器不的指向其他数据,但所指向的指可以改变;
- 若希望迭代器所指的东西不可改变,即实现 const T* 指针,则需要设定为const_iterator,而不是 const iterator。
例如:(第一条)
根据上述分析应修改为:(第二条)const std::vector<int>::iterator it = v.begin(); //注意,此声明只表示迭代器本身是常量 *it = 10; //编译通过,迭代器是常量,但数据可以被修改 ++it; //编译失败!因为const迭代器不允许被改变!
std::vector<int>::const_iterator it = v.begin(); //使用了const_iterator类型 *it = 10; //编译失败,数据不允许被改变! ++it; //编译通过,迭代器本身可以被改变
const与函数
const对函数声明的应用:
在函数声明式内。const可以和函数返回值、参数、函数自身(如果是成员函数)产生关联。
- 另函数返回值为常量,可以降低因客户错误而造成的意外,提高安全性和高校性;
- const 参数,若无必要修改 local对象,应将其声明为 const local对象,避免出现错误修改的问题。
- 例如:
在某处使用此乘法操作符时,误把比较操作符"==“打成了赋值操作符”=":
存在问题:由于 Rational 是自定义类型,因此编译器不会对其报错,因此会产生意想不到的问题,调试是也很难测试。class Rational{....}; Rational operator*(const Rational& lhs, const Rational& rhs){...} Rational a,b,c; if(a*b = c){......}
修改方案:应将操作符的返回值定义为const:const Rational operator*(const Rational& lhs, const Rational& rhs){...}
const与成员函数
const用于成员函数的目的:确保成员函数作用于const对象上。
- 是 class 接口更加直观,可以准确保证那些函数为只读,是否可修改;
- 用 const 修饰的对象只能调用 const 修饰的成员函数,因为不被 const 修饰的成员函数可能会修改其他成员数据,打破 const 关键字的限制。
- 注意:若两个成员函数只有常量性(const)不同,可以被重载。
因此,需要同时声明有const和没有const的成员函数,重载operator[]函数,并对不同版本函数给予不同的返回类型,就会令 const / non-const 得到不同的结果。
例如:const char& operator[](size_t pos) const; char& operator[](size_t pos); 对于某自定义的类Text: Text t("Hello"); const Text ct("Hello"); std::cout<<t[0]; //调用了不加const修饰的索引操作符 std::cout<<ct[0]; //调用了const版本, 但如果只有不加const的操作符,将会报错discard qualifier t[0] = 'x'; //成立,但注意此索引操作符必须声明为引用才可以支持赋值操作 ct[0] = 'x'; //错误!常量不能被修改
注意:const对象大多用于 passed by pointer-to-const 或者 passed by reference-to-const 的传递结果。
如果函数的返回值类型为内置类型,则改动返回值并不会改变其真实值,只会修改其副本。
成员函数的常量性
const成员函数有两种主流观点(或者说通常用法):bitwise constness 和 logical constness。
bitwise const:const成员函数必须用于不改变任何成员变量的情况下,const 成员函数不可以更改对象内任何 non-static 成员变量。
但存在如下情形,即使修改了某个数据,也可以通过编译器的检测:
const Text ct("Hello"); //构造某常量对象
char* pc = &ct[0]; //取其指针
*pc = 'J'; //通过指针修改常量对象,编译不会报错,结果为"Jello"
数据常量性还有另一个局限性,例如:
class Text{
public:
std::sizt_t length() const;
private:
char* pText;
std::size_t length;
bool lengthValid;
....
};
std::size_t Text::length() const{
if(!lengthValid){ //做某些错误检测
length = std::strlen(pText);
lengthValid = true;
}
return length; //这行才是代码核心
}
在这段代码中,length()函数要做某些错误检测,因此可能会修改成员数据。即使真正的功能核心只是返回字符长度,编译器依然认为你可能会修改某些成员数据而报错。
logical constness:允许某些数据被修改,只要这些改动不会反映在外。
可以通过 mutable 关键字(mutable 释放掉 non-static 成员变量的 bitwise constness 约束)来解决 bitwise const 存在的问题:
mutable std::size_t length;
mutable bool lengthValid;
注意:除 mutable 之外,静态成员(static)也可以被const成员函数修改。
在定义常量与非常量成员函数时,避免代码重复
可能大家会有所困惑,既然两个版本的成员函数都要有,为什么又要避免重复?
其实在这里指的是函数的实现要避免重复。试想某函数既要检查边界范围,又要记录读取历史,还要检查数据完整性,这样的代码复制一遍,既不显得美观,又增加了代码维护的难度和编译时间。
因此,我们可以使用非常量的函数来调用常量函数。
const char& operator[](std::size_t pos) const{....}
char& operator[](std::size_t pos){
return
const_cast<char&>( //const_cast去掉const关键字,并转换为char&
static_cast<const Text&>(*this)[position]; //给当前变量加上const关键字,才可以调用const操作符
);
}
为了避免无限递归调用当前非常量的操作符,我们需要将(*this)转换为const Text&类型才能保证安全调用const的操作符,最后去掉const关键字再将其返回,巧妙避免了代码的大段复制。
注意:如果使用相反的方法,用const函数来调用non-const函数,就可能会有未知结果,因为这样相当于non-const函数接触到了const对象的数据,就可能导致常量数据被改变。
总结
- const 是一个十分多功能的关键字,可以用在指针和迭代器以及其reference指涉的对象上、用在函数参数和返回类型上、局部变量或成员函数上,等等;
- 将某些函数、变量声明为 const 可以帮助编译器侦测出错误用法;
- const 可被施加于任何作用于内的对象、函数参数、函数返回类型、成员函数本体;
- 编译器强制实施 bitwise constness,但编写程序时应当使用“概念上的常量性”multable;
- 当 const 和 non-const 成员函数有着实质等价的实现时,令 non-const 版本调用 const 版本可避免代码重复。