关键字是预先保留的标识符,每个关键字都有特殊的含义。程序不能使用与关键字同名的标识符。标准C++有63个关键字,根据关键字的作用,可以将其分为数据类型关键字和流程控制关键字两大类。正确而恰当地使用关键字可以有效地提高程序的质量,起到事半功倍的效果。但如果不恰当地使用这些关键字,则可能会使程序存在某种程度的缺陷,甚至会给程序带来灾难性的后果。本章重点讨论C++的关键字, 以及关键字使用中的陷阱和注意事项,希望能对读者有所帮助。
尽可能多地使用 const
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关键字的功能不仅表现在变量定义方面,在函数声明、函数返回值及类成员函数方面也有着让我们惊讶的表现。
1.函数声明使用const
有些人喜欢把const放到类型的前面,有些人喜欢把const放到类型的后面,但有一点是肯定的,如果有*号必须放在*的前面。按照上面的总结,这两种声明其实并没什么本质的区别。如果你从事MFC开发,下面两个函数的声明也许你不会陌生:
void UpdateUI_1(const CWnd *pWnd);//向UpdateUI_1传入指向CWnd对象常量的指针void UpdateUI_2(CWnd const *pWnd);//UpdateUI_2声明和UpdateUI_1一样
上述两个声明,无论哪种声明都约束pWnd所指向的对象在整个函数执行过程中禁止修改。
2.函数返回值声明为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的结果赋值,编译器报出编译错误
可以看出,声明operator*操作符重载函数的返回值为const可以避免对两个数的运算结果赋值问题。对程序员来说,对两个数的运算结果赋值是非常没道理的。声明operator*的返回值为const可以防止这种情况,所以这样做才是正确的。
小心陷阱
●一个好的用户自定义类型的特征是:它会避免那种没道理的、与固定类型不兼容的行为。
3.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;
}
4.尽量用const常量替换#define常量定义
在C语言中定义一个int型常量,必须这样定义:
#define MAX_LENGTH
而C++则大可不必,因为这种实现存在很多陷阱。如#define只进行字符替换,没有类型安全检查,并且在字符替换时可能会产生意想不到的错误(边际效应)。
我们可以通过const实现常量定义。如上述定义可以用const定义为:
const int MAX_LENGTH = 100;
除了上述陷阱以外,从汇编的角度来看const定义常量,只是给出了对应的内存地址,而不是像#define一样给出的是立即数。const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝,所以使用const常量可以节省内存。
请谨记
●将某些东西声明为const可帮助编译器侦测出错误用法。const可用于任何作用域的对象、函数形参、函数返回值、成员函数本体等。
●尽量用const替换#define, 因为这种替换好处多多。