指针与const:
如果要定义某指针或数据为常量不允许改变:
cpp
const char* p; //数据是const,数据不允许被改变
char* const p; //指针是const,指针不允许被改变
const char* const p; //数据与指针都是const,数据与指针都不可以被改变
记忆法: const在星号左边修饰数据,const在星号右边修饰指针
以及如下两个语句的功能是相同的,不需要对此产生困惑:
cpp
onst char* pw; //都表示指向常量char的指针
char const* pw;
迭代器与const
迭代器在功能上相当于指向某类型T的指针 T*
因此,如果想定义某迭代器指向一个常数,使用const_iterator是不可以的,这样只相当于定义一个迭代器为一个常量(T* const),例如:
cpp
const std::vector<int>::iterator it = v.begin(); //注意,此声明只表示迭代器本身是常量
*it = 10; //编译通过,迭代器是常量,但数据可以被修改
++it; //编译失败!因为const迭代器不允许被改变!
解决方法,使用const_iterator:
cpp
std::vector<int>::const_iterator it = v.begin(); //使用了const_iterator类型
*it = 10; //编译失败,数据不允许被改变!
++it; //编译通过,迭代器本身可以被改变
令函数返回一个常量值,往往可以降低因客户端而造成的意外(尽量使用const可以帮助调试)
cpp
const Rational operator*(const Rational &lhs, const Rational &rhs);
这样可以避免
cpp
if ((a * b) = c)这样的错误。
const成员函数
给成员函数使用const关键字是非常重要的,它可以让接口更加直观,直接告诉用户这个函数是不是只读(Read only),会不会改变某变量。
更重要的是,用const修饰的对象只能调用const修饰的成员函数,因为不被const修饰的成员函数可能会修改其他成员数据,打破const关键字的限制。因此,需要同时声明有const和没有const的成员函数,例如:
cpp
const char& operator[](size_t pos) const;
char& operator[](size_t pos);
对于某自定义的类Text:
cpp
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'; //错误!常量不能被修改
成员函数的常量性(Constness):const成员函数承诺不修改对象的任何成员变量
C++标准对成员函数"常量性"的规定是数据常量性(bitwise constness),即不允许常量对象的成员数据被修改。C++编译器对此的检测也十分简单粗暴,只检查该成员函数有没有给成员数据的赋值操作。
但如下情形,即使修改了某个数据,也可以通过编译器的检测:
cpp
const Text ct("Hello"); //构造某常量对象
char* pc = &ct[0]; //取其指针
*pc = 'K'; //通过指针修改常量对象,编译不会报错,结果为"Kello"
数据常量性还有另一个局限性,例如:
cpp
class Text{
public:
std::size_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关键字来
解决:
cpp
mutable std::size_t length;
mutable bool lengthValid;
这样成员函数length()就可以顺利通过编译。
此外注意, 除mutable之外,静态成员(static)也可以被const成员函数修改。
在定义常量与非常量成员函数时,避免代码重复
可能大家会有所困惑,既然两个版本的成员函数都要有,为什么又要避免重复?
其实在这里指的是函数的实现要避免重复。试想某函数既要检查边界范围,又要记录读取历史,还要检查数据完整性,这样的代码复制一遍,既不显 得美观,又增加了代码维护的难度和编译时间。因此,我们可以使用非常量的函数来调用常量函数。
cpp
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操作符
);
}
为了避免无限递归(如果存在const调用非const,则const调用非const和非const调用const就会生成无限循环)调用当前非常量的操作符,我们需要将(*this)转换为const Text&类型才能保证安全调用const的操作符,最后去掉const关键字再将其返回,巧妙避免了代码的大段复制。
但注意,如果使用相反的方法,用const函数来调用non-const函数,就可能会有未知结果,因为这样相当于non-const函数接触到了const对象的数据,就可能导致常量数据被改变。
只能在非const取消重复,不能在const调用非const
对const成员函数另一种方式解释:
对于成员函数的const,有两个概念:bitwise constness和logical constness。
bitwise constness: 指的是成员函数只有在不更改对象之任何成员变量才可以说是const。也就是说它不更改对象内的任何一个bit。不幸的是许多成员函数虽然不十足具备 const性质却能通过bitwise测试。例如,一个更改了"指针所指物"的成员函数虽然不能算法const,但如果只有指针率属于对象,那么此函数不会引发编译器意义,这导致反直观结果。
cpp
class CTextBlock
{
public:
char &opeartor[](std::size_t position) const //bitwise const声明,但其实不适当
private:
char *pText;
};
cosnt CTextBlock cctb("Hello");
char *pc = &cctb[0];
*pc = 'J';
你创建一个常量对象设以某值,而且只对它调用const成员函数。但你还是改变了它的值。
这种情况导出所谓的logical constness。这一派拥护者主张,一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不出的情况下才得如此。
cpp
class CTextBlock
{
public:
std::size_t length() const;
private:
char *pText;
std::size_t textLength;
bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
if (!lengthIsValid)
{
textLength = std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}
length的实现当然不是bitwise const,因为textLength和lengthIsValid都可能被修改。这两笔数据被修改对于const CTextBlock对象虽然可接受,但编译器不同意。它们坚持bitwise constness。
解决方案很接单:把它们设置成mutable。
const和non const防止重复
cpp
const char &operator[](std::size_t position) const
{
... //边界检验
... //日志记录访问记录
... //检验数据完整性
return text[position];
}
char &operator[](std::size_t position)
{
... //边界检验
... //日志记录访问记录
... //检验数据完整性
return text[postion];
}
这里用non const函数调用const函数的方法来避免代码重复,这种方式并不推荐。
用const调用non const的方法是不合适的,因为non const方法有可能修改数据,而const调用之后,就可能也会修改数据,不符合函数的const语义。
总结:
指针,迭代器,引用,本地变量,全局变量,成员函数,返回值都可以使用const来实现数据只读的目的,const是C++一个非常强大的特性。除此之外,它还能帮助加快调试过程
即使编译器使用数据常量性的标准,我们编程的时候应该采用逻辑常量性,对相关不可避免更改的成员数据加上mutable关键字来修饰
当有大段复制代码出现在const和non-const的成员函数中,可以使用non-const函数来调用const函数来避免复制