条款03:尽可能使用const
条款02讲完了,条款03就针对02中提到的 const 关键字进行了用法上进行了拓展解释。
const 关键字:只要一个变量前用const来修饰,就意味着该变量里的数据只能被访问,而不能被修改,也就是意味着“只读”。
const 关键字主要包括了如下几个应用方面:
a、const 修饰 变量;
b、const 与 STL迭代器;
c、const 在函数中的应用;
一、const 修饰 变量
在这一部分,基本就是条款02中提及的常量用法,把一个变量通过const修饰的方式变成常量,这里的变量,其实要涵盖指针的,在我的理解里,指针其实也是变量,是int型用来存储指向对象地址的特殊变量。
其重要的还是要记住怎么分析const修饰的作用域,经典的分析例子就是const修饰指针,就可以总结为:
const 在星号左边,被 指针指向的变量 为常量;在星号右边,则此指针本身是常量(指针指向的变量依然为变量)。
这样的修饰方式在某些文章中,也又别称,前者叫顶层 const ,底层 const,虽然个人觉得前后更贴切。
还有就是面向对象扩展出来的类成员常量。
类成员常量,表示成员变量在某个对象生存期内是常量,即在对象生成时给出常量值。
而在此对象生存期内,它的值是不能改变的,只能初始化,不能赋值。同一个类的不同对象,不同对象对应的常量数据成员的值可以不同。
常量成员变量只能初始化,其值只能在构造函数中设定,甚至不能在构造函数的函数体中通过赋值设定,只能在构造函数初始化列表中完成 。
二、const 与 STL迭代器
STL,是标准模板库的缩写,其中包含着六个主要部分,容器、算法、迭代器、仿函数、适配器和分配器;
STL 迭代器系以指针为根据塑模出来,所以迭代器的作用就像个T*指针,也因此迭代器同样具有只读的需求和设计。
std::vector<int> vec;
const std::vector<int>::iterator iter1 = vec.begin();
std::vector<int>::const_iterator iter2 = vec.begin();
一句话,按指针的用法来即可。
三、const 在函数中的应用
const在原来c语言函数中的应用,就是对函数进行只读限制,防止一些函数编写者不希望出现的变化。
但本书讨论的是c++,其讨论的部分就要考虑两个部分:改版C语言的一般函数部分 和 面向对象扩展的特殊函数(成员函数)部分。
1、一般函数应用
在一个一般函数里可以被const修饰的就两个部分:返回值和参数。
const修饰返回值,也就是调用函数的结果不希望被修改:
const int Fun(int i_iValue)
{
return 1;
}
const修饰参数,也就是调用函数的参数不希望在函数体内被修改:
int Fun(const int i_iValue)
{
return 1;
}
2、特殊函数(成员函数)应用
特殊函数,其实就是原书中所说的类成员函数,之所以将其拿出来单独说,就是因为其涉及到面向对象中的重载。
即,两个成员函数如果只是常量性不同(const成员函数与非const成员函数),是可以被重载。
额外提一嘴,成员函数的参数用const修饰,其有一个经典的应用场景:拷贝构造函数。
如上的原书例子:
class textBlock
{
public:
char& operator[](std::size_t position){
return text[position];
}
const char& operator[](std::size_t position)const {
return text[position];
}
private:
std::string text;
};
textBlock tb("Hello");
std::cout << tb[0]; //调用非const版本的operator[]
tb[0] = 'x'; //正确
const textBlock ctb("World");
std::cout << ctb[0]; //调用const版本的operator[]
ctb[0] = 'y'; //错误
这里,要针对常量性这个概念进行拓展,其表现为const修饰,但其含义是具有争议的,就是书中提及的 bitwise constness(位常量性) 和 logical constness (逻辑常量性)。
bitwise constness是一个bit都不能动,即严格地要求const成员函数不更改任何成员变量(static除外),这个是C++编译器强制要求 。
class cTextBlock{
public:
size_t length() const;
private:
char pText;
size_t textLength;
bool lengthIsValid;
};
size_t cTextBlock::length() const{
if(!lengthIsValid){
textlength = strlen(pText); //Error!
lengthIsValid= true; //Error!
}
return textLength;
}
logical constness是按逻辑区分哪些bits应该一动不动,哪些bits可以改动,即弹性地允许const成员函数修改一小部分成员变量。
因此,当我们想要的编程模型是logical constness,而编译器是bitwise constness,两者之间可能存在冲突。
class CMyA
{
int m_iValue1;
int m_iValue2;
int* m_pValue;
public:
void Fun1(int index) const { //冲突1:没有修改实际数据,但是const函数中无法修改该值。
m_iValue1++; //Error!
m_iValue2++; //Error!
}
void Fun2(int x) const //冲突2:修改了指针所指的数据,但是编译器仍然认为是const函数。
{
*(m_pValue) = x;
}
};
int main() {
CMyA cA;
return 0;
}
冲突一,存在 在const修饰的成员函数内,必须改变部分成员变量 的需求,但bitwise不允许,解决办法是运用mutable或const_cast;
冲突二,要着重解释一下:
我们明明改变了m_pValue所指向变量的值,但编译器却没有报错。毕竟m_pValue是指针,指针是int变量,存放地址,我们没有m_pValue指向的空间,也就是说地址没有改变,也就是int值就没有改变。
因此,冲突二解决办法,个人觉得没有,只能说,最好的做法是移除成员函数的const,不要让不知道这种情况的人出现错误理解导致更严重后果的发生;
class CMyA
{
mutable int m_iValue1; // mutable关键字,在const函数中也可以修改该值
int m_iValue2;
int* m_pValue;
public:
void Fun1(int index) const { //冲突1:没有修改实际数据,但是const函数中无法修改该值。
m_iValue1++;
const_cast<CMyA*>(this)->m_iValue2++; // const_cast关键字,在const函数中也可以修改该值
}
void Fun2(int x) //冲突2:修改了指针所指的数据,但是编译器仍然认为是const函数。最好的做法:移除const
{
*(m_pValue) = x;
}
};
int main() {
CMyA cA;
return 0;
}
四、总结
1、将某些东西声明为const可帮助编译器侦测处错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
2、编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”。
3、当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
尾语:尽量使用const去提高代码的健壮性。