const允许我们指定一个语义约束(不该被改变的对象),编译器会强制实施这项约束。
const的作用也是比较多的。
- 在class外修饰global或者namespace作用域中的常量。
- 修饰在文件、函数、区块作用域中的被声明为static的变量。
- 修饰classes内被声明为static或者non-static的成员变量。
- 修饰指针或者指针所指物。
char test[] = "this is test.";
char* p = test; //non-const p non- const data
const char* p = test; //non-const p const data
char* const p = test; //const p non-const data
const char* const p = test; //const p const data
判断上面的类型,主要是看关键字const出现在指针 * 的左边还是右边,如果在左边,则表示指针所指的对象是const类型,如果出现在右边,则表示指针本身是const类型。
很多时候,如果指针指向的对象是常量,我们经常会碰到两种实现方式。
void display(const Widget* w);
void display(Widget const* w);
前面我们在学习stl的时候也提到过,stl中的迭代器其实相当于是一个指针(T*)。而stl中也已经声明了相应的const类型的迭代器(const_iterator)。这个时候我们需要注意const iterator。
使用const_iterator的场景一般都出现我们不想改变容器中的值,但需要遍历容器的时候。
typedef vector<double> DVec;
typedef vector<double>::iterator DVecIter;
typedef vector<double>::const_iterator DVecConstIter;
DVec dv;
for(int index = 0; index < 10; ++index)
{
dv.push_back(index);
}
const DVecIter iter = dv.begin();
*iter = 10;
iter++;
DVecConstIter citer = dv.begin();
*citer = 11;
citer ++;
上面的例子中,我们分别定义了两个迭代器 iter 和 citer,对应const类型之后也就是 T* const 和 const T*。也就是说, iter 相当于是一个Double* const
的指针,赋值语句 *iter = 10; 能够正常执行,改变的是所指物的值。但是 iter++;不能通过编译。
citer相当于是一个const Double*
的指针。他指向的对象是const类型的,赋值语句*citer = 11;是不能通过编译的,但是迭代器本身是可改变的。
让函数返回一个常量值,往往会降低编写代码所犯的低级错误而造成的意外。比如下面这个例子:
class Widget{...}
const Widget operator+ (const Widget& lhs, const Widget& rhs);
上面的例子我们返回了一个const类型的Widget对象,为什么呢?
Widget a, b, c;
(a + b) = c;
如果我们出现了上面这样的代码,我们可能是很难理解的吧。猜测一下这行代码的本意应该是两个对象相加之后和对象c对比较,而不是进行赋值。因为在这儿的赋值操作没有任何意义。
但是如果我们将operator+的返回值声明为const类型,则可以避免这种错误。题外话,如果我们需要和一个常量进行比较,习惯性地将常量写在比较 == 的左边,也可以避免这种错误。
我们在写代码的时候,经常会碰到const类型的成员函数,其目的是为了确认该成员函数可作用于const对象上。之所以重要,有两个理由:
- 使类的接口更容易理解,可以很明显的看出来哪些函数可以改变对象内容,而哪些不可以。
- 可操作const对象
我们也经常会有一个经验,使用pass by reference-to-const(const引用)方式传递对象,这样可以提高程序效率。
我们看下面这个例子:
#include<iostream>
using namespace std;
class TextBlack{
public:
TextBlack(char* p) : pText(p)
{
}
const char& operator[](std::size_t position) const
{
return text[position];
}
char& operator[](std::size_t position)
{
return text[position];
}
void print() const
{
std::cout << text<< endl;
}
private:
std::string text;
};
int main()
{
const TextBlack cb("Hello");
//cb[0] = 'J';
TextBlack b("Hello");
b[0] = 'J';
cb.print();
b.print();
return 0;
}
上面的例子中,我们对类TextBlack提供了两个不同返回类型的operator[]方法。并且对这两种方法都进行了测试。
上面被注释掉的操作cb[0] = 'J';
在调用 cb[0]的时候,也就是调用 operator[] 函数的本身是没有错的,错的是对operator[]
函数返回值 const char&
的赋值。
同样的,针对返回值是 non-const 的char&
类型的operator[]也要同样注意,如果返回值是一个char类型的时候,调用 b[0] = ‘J’; 也是错误的,编译器会返回下面错误;
lvalue required as left operand of assignment
这是因为如果函数的返回类型是内置类型,改动函数的返回值是不合法的。纵使合法,C++以by-value返回对象,意味着被改动的其实是 b.text[0]的副本,而不是其本身。
如果我们稍微修改下上面的例子呢?
char& operator[](std::size_t position) const
{
return text[position];
}
const TextBlack cb("Hello");
char* p = &cb[0];
*p = 'J';
这样会不会const类型cb的值呢?实惠改变的。
上面我们一直在强调的是在const成员函数中不能修改成员变量的值。 那么看下下面的例子。
class TextBlack{
public:
TextBlack(string p) : pText(p)
{
}
std::size_t length() const;
private:
std::string pText;
std::size_t m_len;
};
std::size_t TextBlack::length() const
{
if(0 == m_len)
{
m_len = pText.length();
}
return m_len;
}
上面的例子我们定义了一个TextBlack类的const成员函数length,但是在该函数的实现中对成员变量m_len进行了赋值,这是不允许的。
其实m_len的修改对于TextBlack类的const对象来说都是可接受的,但是在编译的时候,编译器是不允许的。
这种情况下,我们可以利用C++中和const有关的一个摆动场 mutable
(可变的)来实现。
mutable std::size_t m_len{0};
在实际编写代码的时候,我们经常会顾及代码的简洁度,其实在上面的第一个例子中,虽然只有简单的一行,也能看到某些代码其实是有重复的,假设我们在实际应用中,需要同时实现下面的函数。
const char& operator[](std::size_t position) const
{
... //一些其他的操作
return text[position];
}
char& operator[](std::size_t position)
{
... //一些其他的操作
return text[position];
}
其中对于一些其他的操作,在const和non-const函数中是一样的,如果写两遍,代码会很长,并且重复性的代码比较多,这可能是我们在很多时候是不能接受的。最简单的方式可能就是将这些操作抽出来,做一个另外的成员函数,然后在这两个函数中调用,虽然代码精简了很多,但还是不可避免的有部分是重复的。
而我们真正需要实现的是,实现一次operator[]
,但是会调用两次。也就是说,我们必须要使其中的一个调用另一个。
我们在很早的时候已经说过C++的强制类型转换,这里,我们也能够通过常量性转移除来实现。
const char& operator[](std::size_t position) const
{
... //一些其他的操作
return text[position];
}
char& operator[](std::size_t position)
{
return const_cast<char&>(static_cast<const TextBlack&>(*this)[position]);
}
我们具体看下上面的实现。
char& operator[](std::size_t position)
首先将 non-const
的 *this
对象通过 static_cast
进行安全的强制转换成const 对象,然后调用 const的成员函数。
最后再将转换后的对象的const属性去除。const_cast操作是会去除一个const 对象的const属性。
如果我们可以让 non-const对象调用 const成员函数,那么反过来呢?是不是也可以。
其实const函数承诺绝不会改变其对象的逻辑状态,但是non-const函数并没有,如果反过来调用,就会违背这项原则。
所以使用const成员函数调用non-const成员函数是错误的,因为对象可能被改变。
- 将某些对象或者变量声明为const类型,可让编译器帮助我们检查错误用法。const可作用于任何作用于内的对象、函数参数、函数返回类型、成员函数本体。
- 如果const和non-const成员函数有实质上的等价,则应使non-const调用const成员函数,可避免代码重复。