您的位置:首页 > 编程语言 > C语言/C++

编写高质量代码:改善C++程序的150个建议(六)

2013-07-27 16:25 375 查看
建议11:将强制转型减到最少

  C++ 在设计中一直强调类型安全,而且也采取了一定的措施来保障这条准则的执行。但是,从C继承而来的强制转型却破坏了C++类型系统,C中的强制转型可谓是“无所不能”,其超强的能力给C++带来了很大的安全隐患。强制转型会引起各种各样的麻烦,有时这些麻烦很容易被察觉,有时它们却又隐藏极深,难以察觉。

  在C/C++语言中,强制转型是“一个你必须全神贯注才能正确使用”的特性。所以一定要慎用强制转型。

  首先来回顾一下C 风格(C-style)的强制转型语法,如下所示:

// 将表达式的类型转换为 T  
(T) expression  
T(expression)
  这两种形式之间没有本质上的区别。在C++中一般称为旧风格的强制转型。

  在赋值时,强制类型的转换形式会让人觉得不精密、不严格,缺乏安全感,主要是因为不管表达式的值是什么类型,系统都自动将其转为赋值运算符左侧变量的类型。而转变后数据可能会有所不同,若不加注意,就可能产生错误。

  将较大的整数转换为较短的数据类型时,会产生无意义的结果,而程序员可能被蒙在鼓里。正如下面的代码片段所示:

unsigned i = 65535;
 
int j = (int) i;
  输出结果竟然成了-1。较长的无符号类型在转换为较短的有符号类型时,其数值很可能会超出较短类型的数值表示范围。编译器不会监测这样的错误,它所能做的仅仅是抛出一条非安全类型转换的警告信息。如果这样的问题发生在运行时,那么一切会悄无声息,系统既不会中断,也不会出现任何的出错信息。

  类似的问题还会发生在有符号负数转化为无符号数、双精度类型转化为单精度类型、浮点数转化为整型等时候。以上这些情况都属于数值的强制转型,在转换过程中,首先生成临时变量,然后会进行数值截断。

  在标准C中,强制转型还有可能导致内存扩张与截断。这是因为在标准C中,任何非void类型的指针都可以和void类型的指针相互指派,也就可以通过void类型指针这个中介,实现不同类型的指针间接相互转换了。代码如下所示:

double PI = 3.1415926;
 
double *pd = Π  
void *temp = pd;
 
int *pi = temp; //转换成功
  指针pd指向的空间本是一个双精度数据,8字节。但是经过转换后,pi却指向了一个4字节的int类型。这种发生内存截断的设计缺陷会在转换后进行内存访问时存在安全隐患。不过,这种情况只会发生在标准C中。在C++中,设计者为了杜绝这种错误的出现,规定了不同类型的指针之间不能相互转换,所以在使用纯C++编程时大可放心。而如果C++中嵌入了部分C代码,就要注意因强制转型而带来的内存扩张或截断了。

  与旧风格的强制转型相对应的就是新风格的强制转型了,在C++提供了如下四种形式:

const_cast(expression)  
dynamic_cast(expression)  
reinterpret_cast(expression)  
static_cast(expression)
  新风格的强制转型针对特定的目的进行了特别的设计,如下所示。

  const_cast<T*> (a)

  它用于从一个类中去除以下这些属性:const、volatile和 __unaligned。

class A  {  // …  };  
void Function()  
{  
    const A *pConstObj = new A;
 
    A *pObj = pConstObj; //ERROR: 不能将const对象指针赋值给非const对象
 
  pObj = const_cast<A*>( pConstObj); // OK
 
    //...  
}
  这种强制转型的目的简单明确,使用情形比较单一,易于掌握。

  dynamic_cast<T*>(a)

它将a值转换成类型为T的对象指针,主要用来实现类层次结构的提升,在很多书中它被称做“安全的向下转型(Safe Downcasting)”,用于继承体系中的向下转型,将基类指针转换为派生类指针,这种转换较为严格和安全。如下面的代码片段所示:

class B {  //...  };  
class D : public B {  //...  };  
void Function(D *pObjD)  
{  
     D *pObj = dynamic_cast<D*>( pObjD);
 
     //...  
}
  如果pObjD指向一个D类型的对象,pObj则指向该对象,所以对该指针执行D类型的任何操作都是安全的。但是,如果pObjD指向的是一个B类型的对象,pObj将是一个空指针,这在一定程度上保证了程序员所需要的“安全”,只是,它也付出了一定的运行时代价,而且代价非常大,实现相当慢。有一种通用实现是通过对类名称进行字符串比较来实现的,只是其在继承体系中所处的位置越深,对strcmp的调用就越多,代价也就越大。如果应用对性能要求较高,那么请放弃dynamic_cast。

  reinterpret_cast<T*>(a)

  它能够用于诸如One_class* 到 Unrelated_class*这样的不相关类型之间的转换,因此它是不安全的。其与C风格的强制转型很是相似。

class A { // ... };  
class B { //... };  
void f()  
{  
  A* pa = new A;
 
  B* pb = reinterpret_cast<B*>(pa);
 
  // ...  
}
  在不了解A、B内存布局的情况下,强行将其进行转换,很有可能出现内存膨胀或截断。

  static_cast<T*>(a)

  它将a的值转换为模板中指定的类型T。但是,在运行时转换过程中,它不会进行类型检查,不能确保转换的安全性。如下面的代码片段所示:

class B { ... };  
class D : public B { ... };  
void Function(B* pb, D* pd)  
{  
    D* pd2 = static_cast<D*>(pb);   // 不安全
 
    B* pb2 = static_cast<B*>(pd);   // 安全的
 
}
  之所以说第一种是不安全的,是因为如果pb指向的仅仅是一个基类B的对象,那么就会凭空生成继承信息。至于这些信息是什么、正确与否,无从得知。所以对它进行D类型的操作将是不安全的。

  C++是一种强类型的编程语言,其规则设计为“保证不会发生类型错误”。在理论层面上,如果希望程序顺利地通过编译,就不应该试图对任何对象做任何不安全的操作。不幸的是,继承自C语言的强制转型破坏了类型系统,所以建议尽量少地使用强制转型,无论是旧的C风格的还是新的C++风格的。如果发现自己使用了强制转型,那么一定要小心,这可能就是程序出现问题的一个信号。

  请记住:

  由于强制转型无所不能,会给C++程序带来很大的安全隐患,因此建议在C++代码中,努力将强制转型减到最少。

  建议12:优先使用前缀操作符

  也许从开始接触C/C++程序的那天起,就记住了前缀和后缀运算,知道了++的前缀形式是“先加再用”,后缀形式是“先用再加”。前缀和后缀运算是C和C++语言中的基本运算,它们具有类似的功能,区别也很细微,主要体现在运行效率上。

  分析下面的代码片段:

int n=0, m=0;
 
n = ++m; /*m先加1, 之后赋给n*/  
cout << n << m; /*结果:1 1*/
  在这个例子中,赋值之后,n等于1,因为它是在将m赋予n之前完成的自增操作。再看下面的代码:

int n=0, m=0;
 
n = m++; /*先将m赋于n, 之后m加1*/
 
cout << n << m; /*结果:0 1*/
这个例子中,赋值之后,n等于0,因为它是先将m赋予n,之后m再加1的。

  为了更好地理解前缀操作符和后缀操作符之间的区别,可以查看这些操作的反汇编代码。即使不了解汇编语言,也可以很清楚地看到二者之间的区别,注意inc指令出现的位置:

/* m=n++;的反汇编代码*/
 
mov ecx, [ebp-0x04] /*store n's value in ecx register*/  
mov [ebp-0x08], ecx /*assign value in ecx to m*/  
inc dword ptr [ebp-0x04] /*increment n*/  
/*m=++n;的反汇编代码*/  
inc dword ptr [ebp-0x04] /*increment n;*/  
mov eax, [ebp-0x04] /*store n's value in eax register*/  
mov [ebp-0x08], eax /*assign value in eax to m*/
  从汇编代码可以看出,两者采取了相同的操作,只是顺序稍有不同而已。但是,前缀操作符的效率要优于后缀操作符,这是因为在运行操作符之前编译器需要建立一个临时的对象,而这还要从函数重载说起。

  重载函数间的区别取决于它们在参数类型上

ClassName & ClassName::operator++()  
{  
 ClassAdd (1); //increment  current object  
 return *this; //return by reference the current object  
}  
 
ClassName ClassName::operator++(int unused)  
{  
 ClassName temp(*this); //copy of the current object  
 ClassAdd (1); //increment current object  
 return temp;  //return copy  
}
的差异,但不论是自增的前缀还是后缀,都只有一个参数。为了解决这个语言问题,C++规定后缀形式有一个int类型的参数,当函数被调用时,编译器传递一个0作为int类型参数的值给该函数:

//成员函数形式的重载  
< Type > ClassName :: operator ++ ( ); // 前缀
 
< Type > ClassName :: operator ++ ( int ); // 后缀
 
// 非成员函数形式的重载  
< Type > operator ++ (ClassName & ); // 前缀
 
< Type > operator ++(ClassName &,int); // 后缀
  在实现中,后缀操作会先构造一个临时对象,并将原对象保存,然后完成自增操作,最后将保存对象原值的临时对象返回。代码如下所示:

ClassName & ClassName::operator++()  
{  
 ClassAdd (1); //increment  current object  
 return *this; //return by reference the current object  
}  
 
ClassName ClassName::operator++(int unused)  
{  
 ClassName temp(*this); //copy of the current object  
 ClassAdd (1); //increment current object  
 return temp;  //return copy  
}
  由于前缀操作省去了临时对象的构造,因此它在效率上优于后缀操作。不过,在应用到整型和长整型的操作时,前缀和后缀操作在性能上的区别通常是可以忽略的。但对于用户自定义类型,这还是非常值得注意的。当然就像80-20规则告诉我们的那样,如果在一个很大的程序里,程序数据结构和算法不够优秀,它所能带来的效率提升也是微不足道的,不能使大局有所改变。但是既然它们有差异,我们为什么不在必要的时候采用更有效率的呢?

  请记住:

  对于整型和长整型的操作,前缀操作和后缀操作的性能区别通常是可以忽略的。对于用户自定义类型,优先使用前缀操作符。因为与后缀操作符相比,前缀操作符因为无须构造临时对象而更具性能优势。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  c++