在C++中,const是一个威力巨大的关键字,它在提高程序性能以及代码健壮性、侦测潜在的未知错误和简化代码方面,有不可忽略的功劳。
Scott Meyers的经典C++著作《Effective C++》拿出单独的一章来介绍const,并且在多个章节中介绍const特性的具体功能实现,从中我们可以知道const在C++中的重要性。
const是指定一种语义约束,指明这个变量不应该被改动,或者指明一个函数不应该在其内部改变变量的状态。这种约束是由编译器强制实现。这就避免了由于我们的疏忽而导致的无法侦测的错误。const的行为多种多样,它可以修饰:
1.常量(包括指针)
2.函数体
3.函数形式参数与返回类型
4.与static组合使用
1.修饰常量
const在C++中用法最多的便是定义一个常量。const定义的常量可以在global作用域,也可以定义在一个class作用域中。当在global作用域中定义一个常量的时候,必须指定一个初始值,如:const int a = 10; 但是,在类的内部定义一个常量的,同样必须给定一个初始值,但是这个初始值不应该像global作用域中一样,直接赋值。例如:
class Test
{
public:
Test() {}
// other function
private:
const int a = 10;
};
这种写法是错误的,因为C++规定,所有成员变量的初始化,必须放在构造函数当中,也就是说,我们必须将a放在Test()函数中进行初始化。关于类成员对象的初始化,有必要多说一句。在C++的构造函数中,很多人认为,函数的初始化是在进入的构造函数后才进行的。其实这是错误的。构造函数的功能分为两个部分:1.初始化阶段 2 普通计算阶段。也就是说,在进入到构造函数体内之前,成员变量会完成初始化,进入函数体内的所谓的“初始化行为”,其实是赋值。在进入函数体之前如果初始化呢?C++构造函数为了确保所有成员变量进入到函数体之前被初始化,提供了一种机制,叫做“成员初始化列表”。写法如下:
class Test
{
public:
Test() : a(10)
{}
// other function
private:
const int a ;
};
在进入构造函数之前,用冒号表明这是一个成员初始化列表,然后依次初始化每一个成员变量。成员变量的初始化次序是跟声明顺序相同。而且C++规定,const常量与引用必须在成员初始化列表中初始化。因为const与引用的性质就是在初始化的时候必须明确的给定一个值。如果进入到构造函数体中,则不再是初始化,而是赋值了。为了提高效率,所有的大对象(一般是我们自己定义的类型以及STL类型)应该在构造函数初始化列表中初始化。
言归正传,在一个类内部定义一个const常量,同样也可以跟static组合,变成一个类的专属常量。它的初始化方式不再是上面我们所说的在构造函数里面进行初始化。而是类似于普通的static对象。这里就不在赘述了。
关于const常量,不得不提使用const修饰指针。const可以修饰指针,也可以修饰指针所指物。Effective C++上对于这一块讲解的相当的详细。“如果关键在const出现在*左边,表示被指物是常量,如果出现在*右边,表明指针自身是常量。如果出现在*两边,表明被指物跟指针两者都是常量”(《Effective C++ 3rd》P18)。所以说,以下两种写法要表达的意思是相同的:
int const * p = &a;
const int *p = &a;
2.修饰函数体
在一个类中,如果我们希望这个函数不应该改变调用者的状态,则应该明确的告诉编译器,这是一个const成员函数,使用方法便是在函数声明后面添加关键字const。const成员函数是操作const对象成为可能。还有一点需要注意,如果两个成员函数只是const属性不同的话,可以重载。
关于const成员函数,Effective C++中讲到一点,就是关于在简化程序方面,通过两次强制转化,使用non-const版本的成员函数去调用const版本的成员函数。如果两个成员函数只有const属性不同的话,这样做可以极大的简化程序代码,是程序看上去简洁,便于理解。但是,由于使用了强制类型转换,也存在潜在的安全性问题。当遇到这种情况的时候,你可以在安全性与代码易读性两者之间权衡,找到最适合自己的解决方案。
现在我们可以看Scott Meyers给的例子:
class TextBlock
{
public:
TextBlock() {}
~TextBlock() {}
// 成员函数的non-const版本
char& operator[] (size_t position)
{
return text[position];
}
// 成员函数的const版本
const char& operator[] (size_t position) const
{
return text[position];
}
private:
std::string text;
};
我们定义了两个成员函数,一个const版本,和一个non-const版本,两者除了const属性不同以外,函数的内容完全相同。这个例子很简单,但是,如果这两个函数的函数体是几十甚至上百行的话,有时候自己看到重复的两段代码,心里都有想砸电脑的冲动。此时,我们可以使用non-const版本去调用const版本,但是需要进行两次强制转化。一次是对*this添加const属性,另一个是对返回值去除const属性。此时的代码修改如下:
class TextBlock
{
public:
TextBlock() {}
~TextBlock() {}
const char& operator[] (size_t position) const
{
return text[position];
}
// 调用const版本
char& operator[] (size_t position)
{
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
private:
std::string text;
};
如果我们反向的调用,即使用const调用non-const版本来是程序简化,避免重复的方法是不行的。因为const成员函数保证不会改变对象的逻辑状态,但是non-const则不会有这种限制,所以,如此的调用会违背const成员函数的基本功能,因为对象可能被改动。可能有人会想,我们可以使用强制转换,将const属性移除以后再去调用它的non-const版本,但是,这会产生一些潜在的安全问题。因为non-const可以对对象做任何操作,而const成员函数是保证对象的逻辑状态不会发生任何改变的。相反,我们使用non-const去调用const版本的成员函数则是安全的。
注:成员函数的const属性意味着什么?可以参看《Effective C++》条款2 P21。很有意思的一次哲学思辨。