巧妙编码,减少C++编程麻烦
编程爱好者
本篇介绍的是C++的一些常用技巧。这些技巧看起来好像很不起眼,在实际应用当中却起着重要作用。这些技巧虽然是谈不上可以提高程序的编写效率,却可以为你去除一些不必要的麻烦和一些程序中不易被发现的隐患。
1.都是“=”惹的祸
先来看看下面两个判断表达式的写法有什么不同:
// 第一种写法:
if( value == 0 )
{
……
}
// 第二种写法:
if( 0 == value ) // 推荐!
{
……
}
value是一个变量,两个判断表达式都是判断变量value是否等于0。所不同的是,两个判断表达式的变量value和常量0的位置刚好对调了。第一个是“value == 0”,另一个是“0 == value”。
许多程序员并不知道这两种写法有什么不同,他们通常使用的写法是“value == 0”,我在这里推荐大家最好写成“0 == value”这种形式。
两个写法有什么不同吗?第二种写法是否比第一种写法更有效率?很遗憾,两种写法的效率是一样的。那么为什么我要推荐大家使用第二种写法呢?原因是在C++里判断两个值是否相等使用的运算符是两个等于号(==),没有人能百分之百地保证自己不会漏写一个等号。使用第一种表达式的写法,如果漏写了一个等号的话就变成下面的样子:
if( value = 0 ) // 严重的错误,却没有任何的错误提示和警告
{……
}
上面的判断表达式,原本的意图是想判断变量value是否等于0的,但由于漏写了一个等于号,却变成了给变量value赋值为0。我们知道,在条件判断中,如果条件表达式的值是非0的话,条件就为真,否则为假。像上面那样写法,条件判断就永远为假了(如果是非0的话就永远为真)。像这样漏写一个等号的情况,条件表达式的值要么永远为真,要么永远为假。这种判断两个值是否相等的条件表达式,程序中应该有许多,有一两处漏写了一个等号并不足为奇,可怕的是程序一般都可以正常运行,而且有许多情况并不易察觉得出来有什么异样。编译器更不会给出任何的错误提示或警告,因为这种表达式是完全合法的。
如果是换成第二种写法,而又那么不小心漏写一个等号的话,程序变成下面的样子:
if( 0 = value ) // 不合法的表达式,编译器将给出错误提示
{
……
}
编译程序,编译器给出以下的错误提示:
error C2106: “=” : 左操作数必须为 l 值
因此,我们可以轻松地知道哪里漏写了一个等号。
2.避免重定义
如果把一个struct定义放在一个头文件中,就有可能在一个编译程序中多次包含这个头文件。编译器认为重定义是一个错误。如下面的例子:
// file : type.h
struct type01
{
int a,b,c;
};
// file : a.h
#include "type.h"
……
// file : b.h
#include "type.h"
……
// file main.cpp
#include "a.h"
#include "b.h"
int main(void)
{
……
}
编译程序,编译器给出以下的错误提示:
error C2011: “type01” : “struct”类型重定义
原因是头文件type.h定义了一个struct类型type01,头文件a.h和b.h都包含了头文件type.h。而在main.cpp文件里却同时包含了头文件a.h和b.h。因此出现了重定义的错误。
可以通过像以下那样改写type.h文件,从而避免重定义错误:
// file : type.h
#ifndef __TYPE_H__ // 如果标记没被设置
#define __TYPE_H__ // 设置标记
struct type01
{
int a,b,c;
};
#endif // End of __TYPE_H__
通过这样改写type.h文件后,程序可以顺利编译过去了。
我们是通过测试预处理器的标记来检查type.h头文件是否已经包含过了。如果这个标记没有设置,表示这个头文件没有被包含过,则应该设计标记。反之,如果这个标记已经设置,则表示这个头文件已经被包含,所以应该忽略。
3.安全地释放内存
在定义一个指针时,通常我们都会给它一个默认值NULL(也就是0),表示没有指向任何变量,如下:
CPerpon *pPerpon = NULL;
在使用一个指针时通常都会先判断这个指针的值是否为NULL。不为NULL才可以使用该指针。如下:
if( pPerpon != NULL )
{
……
}
在释放一个指针指向的内存时,一般在调用了delete操作符后,都要把指针的值设为NULL。否则程序将无法判断这个指针是否真的指向一个变量。
delete pPerpon;
pPerpon = NULL;
但是在释放一个指针指向的内存时,大多数情况都要重写上面的两句代码(程序员一般都很懒),而且不避免会出现这样的情况,在delete一个指针后忘了把指针设为NULL。
这时,这些懒惰而善忘的程序员就会用以下这样一个宏来安全地释放内存:
#define SAFE_DELETE(p) { if(p) { delete p; p = NULL; } }
同理,下面是安全释放一个数组的宏:
#define SAFE_DELETE_ARRAY(p) { if(p) { delete[] p; p = NULL; } }
4.尽量把类设计成黑盒子
当我们要用到一个类里面的数据成员时,最简单的做法是将类的数据成员声明成公有的(public)。但是这样做的话,类的设计者就无法控制何时访问这些数据成员了。一个类的数据成员应该尽量地保证在类里面得到控制。
较好的做法是把类的数据成员声明为private或protected,然后写一个get函数来取得数据成员的值。如下:
class CPerpon
{ public:
std::string get_name(void) const
{ return m_strName; }
private:
std::string m_strName;};
如果类的成员函数不会改变数据成员的值,最好把这个成员函数声明为const。因为const的类对象无法访问非const的成员函数。
如果要设置类的数据成员的值,可以为类写一个set函数。如下:
void CPerson::set_name( std::string name )
{ m_strName = name;}
如果数据成员不能被设置成某些值,可以在set函数里作出判断。从而我们可以做到:一个类的数据成员在类里面得到控制。
5.正确地赋值
在为一个类重载了赋值操作符时,很多人都是像下面那样写:
class string
{ public:
string& operator = ( const string &str );
private:
char *data;};
string& string::operator = ( const string & str )
{ delete[] data;
data = new char[ strlen( str.data ) + 1 ];
strcpy( data, s.data );
return *this;}
如果是两个不同的string对象进行赋值,这种写法是不会出什么问题的。但是,如果是自我赋值呢?这时问题就来了:赋值时是先删除了this的data,再把str的data的数据复制到this的data。但是,自我赋值时this和str都指向同一个对象,很明显这种做法完全错误!
为了解决这个问题,可以使用下面的方法:
string& string::operator = ( const string & str )
{ if( this != &str )
{ delete[] data;
data = new char[ strlen( str.data ) + 1 ];
strcpy( data, s.data ); }
return *this;}
通过这样改写后,只有非自我赋值时才会做复制操作。
编后:
通过上面的文章,大家可以看出,在编程中,有一些细节是值得我们注意的,否则,出错了也很难找到原因。养成好的书写代码习惯对程序员来说,很重要!