const可以说是C++中最为神奇的关键字。它的神奇在于:你可以通过编译器指定语义上的约束,而不需要花费任何的代价。例如,你可以通过const关键字告诉编译器和其他程序员,你程序中的某个数值需保持恒定不变,不论何时只要你这么做了,编译器就会协助你保证此约束不被破坏。
const关键字的用途是多种多样的。例如:在类的外部,你可以定义全局作用域的常量,也可以通过添加static来定义文件、函数或程序块作用域的常量。对于指针,通过const你可以定义指针是const、其所指向的数据是const或两者都是const。例如:
char szGreeting[] = “Hello! My God!”;
char *pszGreeting = szGreeting; // 非 const 指针,非 const 数据
const char * pszGreeting = szGreeting; // 非 const 指针, const 数据
char *const pszGreeting = szGreeting; // const 指针, 非 const 数据
const char *const pszGreeting = szGreeting; // const 指针, const 数据
上述语法也许让你眩晕,不知如何是好?其实并不然,如果你仔细研究,你会发现这里面的规律。规律总结如下:
- 如果const在*的左边,说明指针所指向对象是常值;如果所指向对象为常值,const在类型的前面和后面其实都一样。
- 如果const在*的右边说明指针是恒值;
- 最复杂的是const同时出现在*的左右两侧,此时说明指针是恒值,指针所指向的对象也是恒值。
const关键字功能不仅表现在变量定义,在函数声明、函数返回值及类成员函数方面,也有着让我们惊讶的表现。
函数声明使用const
有些人喜欢把const放到类型的前面,有些人喜欢把const放到类型的后面,但有一点是肯定的,如果有*号必须在*的前面。按照上面的总结,这两种声明其实并没什么本质的区别。如果你从事MFC开发,下面看两个函数的声明也许你不会陌生。
void UpdateUI_1(const CWnd *pWnd);// 向UpdateUI_1传入指向CWnd对象常量的指针
void UpdateUI_2(CWnd const *pWnd);// UpdateUI_2声明和UpdateUI_1一样。
上述两声明,无论哪种声明都约束pWnd所指向的对象在整个函数执行过程中禁止修改。
函数返回值声明为const
让函数的返回一个常量值,经常可以在不降低安全性和效率的前提下减少用户出错的概率。为了说明这个问题,我们先看下面的代码片段:
CRational {} // 有理数类,支持有理数的加减乘除四则运算。
CRational operator*(const rational& lhs, const rational& rhs);// 有理数乘法运算。
CRational a, b, c;
......
(a * b) = c; // 对a*b的结果赋值,不符合逻辑。但可以通过编译。
我不知道为何有些程序员会想到对两个数的运算结果直接赋值,但我们却知道:如果a、b和c是固定类型,这种做法显然是不合法的。但不幸的是:上述代码片段虽然不合法,但C++编译器却没有检测出来。明白了其中的问题,我们在看下面的代码片段。
CRational {} // 有理数类,支持有理数的加减乘除四则运算。
const CRational operator*(const rational& lhs, const rational& rhs); // 有理数乘法运算。
CRational a, b, c;
......
(a * b) = c; // 对a*b的结果赋值,编译器报出编译错误。
可以看出,声明operate *操作符重载函数的返回const可以避免对两个数运算结果赋值问题。对我们来说,对两个数的运算结果赋值是非常没道理的。声明operator *的返回值为const可以防止这种情况,所以这样做才是正确的。
小心陷阱
- 一个好的用户自定义类型的特征是:它会避免那种没道理的与固定类型不兼容的行为。
- 声明函数返回const类型可避免那些没道理的与固定类型不兼容的行为。
const 成员函数
函数具有const属性,这是C++所特有的特征。将成员函数声明为const就是指明这个函数可以被const对象调用。
const成员函数优点:
- const成员函数可使得类的接口更加易于理解。
- const成员函数可以与 const 对象协同工作。这是高效编码十分重要的一个方面。
如果成员函数之间的区别仅仅为“是否是 const 的”,那么它们可被重载。很多人都忽略了这一点,但是这是 C++ 重要特征之一。
说了这么多,现在我们讨论把一个成员函数声明为const到底有什么玄机?这里面有两个说法:按位恒定和逻辑恒定。
按位恒定坚持:当且仅当一个成员函数对所有的数据成员都不作出改动时,才需要将此函数声明为const。也就是说如果一个成员函数声明const的条件是:成员函数不对对象内部做任何的修改。按位恒定的一个好处就是:它使得错误检查变得更轻松。但不是一个成员函数声明了const,它就不会修改类对象的数据成员。下面这个例子就是这样,虽然成员函数声明了const属性,但它依然可以修改类对象的数据成员。这显然是有问题的。
class CHString // 自定义CHstring类,类似STL中的string类
{
public:
char& operator[](std::size_t position) const // 定义CHstring类,[]中括号运算符。
{
return pText[position];
}
private:
char *pText;
};
const CHString cctb("Hello"); // 声明对象常量
char *pc = &cctb[0]; // 调用 const 的 operator[]
// 从而得到一个指向 cctb 中数据的指针
*pc = 'J'; // cctb 现在的值为 "Jello"
于是逻辑恒定论便应运而生了。逻辑恒定坚持:一个 const 的成员函数可能对其调用的对象内部做出改动,但是仅仅以客户端无法察觉的方式进行。利用可变的( mutable )数据成员可实现这一目标。 (mutable 可以使非静态数据成员不受按位恒定规则的约束):
// 字符串输出次数统计实现类,统计字符串被使用了多少次
class ClxTest
{
public:
ClxTest();
~ClxTest();
// 输出字符串
void Output() const;
// 获得字符串输出次数
int GetOutputTimes() const;
private:
// 字符串输出次数
mutable int m_iTimes;
};
// 构造函数
ClxTest::ClxTest()
{
m_iTimes = 0;
}
// 析构造函数
ClxTest::~ClxTest()
{
}
// 输出字符串。
void ClxTest::Output() const
{
cout << "Output for test!" << endl;
m_iTimes++;
}
// 字符串输出次数。
int ClxTest::GetOutputTimes() const
{
return m_iTimes;
}
// 字符串输出测试。
void OutputTest(const ClxTest& lx)
{
cout << lx.GetOutputTimes() << endl;
lx.Output();
cout << lx.GetOutputTimes() << endl;
}
尽量用const常量替换#define常量定义
C语言中定义一个int型常量,我们必须这么定义:
#define MAX_LENGTH 100
而C++则大可不必,因为这种实现存在很多陷阱。如#define只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
我们可以通过const实现常量定义。如上述定义可以用const定义为
const int MAX_LENGTH = 100;
除了上述陷阱以外,从汇编的角度来看const定义常量,只是给出了对应的内存地址,而不是像#define一样给出的是立即数。const定义的常量在程序运行过程中只有一份拷贝,而 #define定义的常量在内存中有若干个拷贝。所以使用const常量可以节省内存。
请谨记
- 将某些东西声明为const可帮助编译器侦测出错误用法。const可用于任何作用域的对象,函数形参,函数返回值,成员函数本体等。
- 尽量用const替换#define,因为这种替换好处多多。