第五章 表达式(part3) 复合表达式的求值、new 和 delete 表达式、类型转换

本文深入探讨了C++中复合表达式的求值顺序、优先级、结合性和内存管理,包括如何处理优先级规则、结合性和求值顺序,以及动态内存分配和回收的正确做法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

5.10. 复合表达式的求值

含有两个或更多操作符的表达式称为复合表达式

在复合表达式中,操作数和操作符的结合方式决定了整个表达式的值。表达式的结果会因为操作符和操作数的分组结合方式的不同而不同。

操作数的分组结合方式取决于操作符的优先级和结合性。

也就是说,优先级和结合性决定了表达式的哪个部分用作哪个操作符的操作数。

如果程序员不想考虑这些规则,可以在复合表达式中使用圆括号强制实现某个特殊的分组

《Note》:

优先级规定的是操作数的结合方式,但并没有说明操作数的计算顺序。在大多数情况下,操作数一般以最方便的次序求解。

5.10.1. 优先级
圆括号凌驾于优先级之上

我们已经通过前面的例子了解了优先级规则如何影响程序的正确性。例如,表达式:

     *iter++;

其中,++ 的优先级高于*操作符,这就意味着 iter++ 先结合。而操作符 * 的操作数是iter 做了自增操作后的结果。

如果我们希望对 iter 所指向的值做自增操作,则必须使用圆括号强制实现我们的目的:

     (*iter)++; // increment value to which iter refers and yield unincremented value
圆括号指明操作符 * 的操作数是 iter,然后表达式以 *iter 作为 ++操作符的操作数。


5.10.2. 结合性

结合性规定了具有相同优先级的操作符如何分组。我们已经遇到过涉及结合性的例子。

其中之一使用了赋值操作的右结合性,这个特性允许将多个赋值操作串接起来:

     ival = jval = kval = lval       // right associative
     (ival = (jval = (kval = lval))) // equivalent, parenthesized version

该表达式首先将 lval 赋给 kval ,然后将 kval 的值赋给jval ,最后将 jval 的值再赋给 ival

另一方面,算术操作符为左结合。表达式

     ival * jval / kval * lval       // left associative
     (((ival * jval) / kval) * lval) // equivalent, parenthesized version

先对 ivaljval 做乘法操作,然后乘积除以 kval,最后再将其商与lval 相乘。


5.10.3. 求值顺序

我们讨论了 &&|| 操作符计算其操作数的次序:当且仅当其右操作数确实影响了整个表达式的值时,才计算这两个操作符的右操作数。

根据这个原则,可编写如下代码:

     // iter only dereferenced if it isn't at end
     while (iter != vec.end() && *iter != some_val)

C++中,规定了操作数计算顺序的操作符还有条件(?:)和逗号操作符。除此之外,其他操作符并未指定其操作数的求值顺序。

例如,表达式

     f1() * f2();

在做乘法操作之前,必须调用 f1 函数和 f2 函数,毕竟其调用结果要相乘。然而,我们却无法知道到底是先调用f1 还是先调用 f2

《Note》:

其实,以什么次序求解操作数通常没有多大关系。只有当操作符的两个操作数涉及到同一个对象,并改变其值时,操作数的计算次序才会影响结果。

如果一个子表达式修改了另一个子表达式的操作数,则操作数的求解次序就变得相当重要:

     // oops! language does not define order of evaluation
     if (ia[index++] < ia[index])

此表达式的行为没有明确定义。问题在于:< 操作符的左右操作数都使用了 index 变量,

但是,左操作数更改了该变量的值。假设 index 初值为 0,编译器可以用下面两种方式之一求该表达式的值:

     if (ia[0] < ia[0]) // execution if rhs is evaluated first
     if (ia[0] < ia[1]) // execution if lhs is evaluated first

可以假设程序员希望先求左操作数的值,因此 index 的值加 1。

如果是这样的话,比较 ia[0]ia[1] 的值。

然而,C++ 语言不能确保从左到右的计算次序。

事实上,这类表达式的行为没有明确定义。

一种实现可能是先计算右操作数,于是 ia[0] 与自己做比较,要不然就是做完全不同的操作。

建议:复合表达式的处理 

初学 C 和 C++ 的程序员一般很难理解求值顺序、优先级和结合性规则。

误解表达式和操作数如何求解将导致大量的程序错误。

此外,除非程序员已经完全理解了相关规则,否则这类错误很难发现,因为仅靠阅读程序是无法排除这些错误的。

下面两个指导原则有助于处理复合表达式:

  1. 如果有怀疑,则在表达式上按程序逻辑要求使用圆括号强制操作数的组合。

  2. 如果要修改操作数的值,则不要在同一个语句的其他地方使用该操作数。如果必须使用改变的值,则把该表达式分割成两个独立语句:在一个语句中改变该操作数的值,再在下一个语句使用它。

第二个规则有一个重要的例外:如果一个子表达式修改操作数的值,然后将该子表达式的结果用于另一个子表达式,这样则是安全的。

例如,*++iter 表达式的自增操作修改了 iter 的值,然后将 iter(修改后)的值用作 * 操作符的操作数。

对于这个表达式或其他类似的表达式,其操作数的计算次序无关紧要。而为了计算更复杂的表达式,改变操作数值的子表达式必须首先计算。

这种方法很常用,不会产生什么问题。

《Beware》:

一个表达式里,不要在两个或更多的子表达式中对同一对象做自增或自减操作。

以一种安全而且独立于机器的方式重写上述比较两个数组元素的程序:

     if (ia[index] < ia[index + 1]) {
         // do whatever
     }
     ++index;
现在,两个操作数的值不会相互影响。

5.11. newdelete 表达式

定义变量时,必须指定其数据类型和名字。而动态创建对象时,只需指定其数据类型,而不必为该对象命名。取而代之的是,new 表达式返回指向新创建对象的指针,我们通过该指针来访问此对象:

     int i;              // named, uninitialized int variable
     int *pi = new int;  // pi points to dynamically allocated,
                         // unnamed, uninitialized int
这个 new 表达式在自由存储区中分配创建了一个整型对象,并返回此对象的地址,并用该地址初始化指针 pi


动态创建对象的初始化

动态创建的对象可用初始化变量的方式实现初始化:

     int i(1024);              // value of i is 1024
     int *pi = new int(1024);  // object to which pi points is 1024
     string s(10, '9');                   // value of s is "9999999999"
     string *ps = new string(10, '9');    // *ps is "9999999999"

C++ 使用直接初始化(direct-initialization)语法规则初始化动态创建的对象。

如果提供了初值,new 表达式分配到所需要的内存后,用给定的初值初始化该内存空间。

在本例中,pi 所指向的新创建对象将被初始化为 1024,而 ps 所指向的对象则初始化为十个9的字符串。

动态创建对象的默认初始化

如果不提供显式初始化,动态创建的对象与在函数内定义的变量初始化方式相同。

对于类类型的对象,用该类的默认构造函数初始化;而内置类型的对象则无初始化。

     string *ps = new string; // initialized to empty string
     int *pi = new int;       // pi points to an uninitialized int
通常,除了对其赋值之外,对未初始化的对象所关联的值的任何使用都是没有定义的。

正如我们(几乎)总是要初始化定义为变量的对象一样,在动态创建对象时,(几乎)总是对它做初始化也是一个好办法

同样也可对动态创建的对象做值初始化(value-initialize):

     string *ps = new string();  // initialized to empty string
     int *pi = new int();  // pi points to an int value-initialized to 0
     cls *pc = new cls();  // pc points to a value-initialized object of type cls

以上表明程序员想通过在类型名后面使用一对内容为空的圆括号对动态创建的对象做值初始化。

内容为空的圆括号表示虽然要做初始化,但实际上并未提供特定的初值。

对于提供了默认构造函数的类类型(例如 string),没有必要对其对象进行值初始化:

无论程序是明确地不初始化还是要求进行值初始化,都会自动调用其默认构造函数初始化该对象。

而对于内置类型或没有定义默认构造函数的类型,采用不同初始化方式则有显著的差别:

     int *pi = new int;         // pi points to an uninitialized int
     int *pi = new int();       // pi points to an int value-initialized to 0

第一个语句的 int 型变量没有初始化,而第二个语句的 int 型变量则被初始化为0。

《Beware》:值初始化的 () 语法必须置于类型名后面,而不是变量后。

int x(); // does not value initialize x
这个语句声明了一个名为 x、没有参数而且返回 int 值的函数。



耗尽内存
尽管现代机器的内存容量越来越大,但是自由存储区总有可能被耗尽。如果程序用完了所有可用的内存, new 表达式就有可能失败。

如果 new 表达式无法获取需要的内存空间,系统将抛出名为 bad_alloc 的异常。

撤销动态创建的对象

动态创建的对象用完后,程序员必须显式地将该对象占用的内存返回给自由存储区。C++ 提供了delete 表达式释放指针所指向的地址空间。

     delete pi;

该命令释放 pi 指向的 int 型对象所占用的内存空间。

《Note》:

如果指针指向不是用 new 分配的内存地址,则在该指针上使用 delete 是不合法的。

C++ 没有明确定义如何释放指向不是用 new 分配的内存地址的指针。

下面提供了一些安全的和不安全的 delete expressions 表达式。

     int i;
     int *pi = &i;
     string str = "dwarves";
     double *pd = new double(33);
     delete str; // error: str is not a dynamic object
     delete pi;  // error: pi refers to a local
     delete pd;  // ok

值得注意的是:编译器可能会拒绝编译 strdelete 语句。编译器知道 str 并不是一个指针,因此会在编译时就能检查出这个错误。

第二个错误则比较隐蔽:通常来说,编译器不能断定一个指针指向什么类型的对象,因此尽管这个语句是错误的,但在大部分编译器上仍能通过。

如果指针的值为 0,则在其上做 delete 操作是合法的,但这样做没有任何意义:

     int *ip = 0;
     delete ip; // ok: always ok to delete a pointer that is equal to 0
C++ 保证:删除 0 值的指针是安全的。

delete 之后,重设指针的值

执行语句

     delete p;

后,p 变成没有定义。在很多机器上,尽管 p 没有定义,但仍然存放了它之前所指向对象的地址,然而p 所指向的内存已经被释放,因此 p 不再有效。

删除指针后,该指针变成悬垂指针。悬垂指针指向曾经存放对象的内存,但该对象已经不再存在了。悬垂指针往往导致程序错误,而且很难检测出来。

《Best》:

一旦删除了指针所指向的对象,立即将指针置为 0,这样就非常清楚地表明指针不再指向任何对象。


const 对象的动态分配和回收

C++ 允许动态创建 const 对象:

     // allocate and initialize a const object
     const int *pci = new const int(1024);

与其他常量一样,动态创建的 const 对象必须在创建时初始化,并且一经初始化,其值就不能再修改。

上述 new 表达式返回指向intconst 对象的指针。与其他 const 对象的地址一样,由于 new 返回的地址上存放的是const 对象,因此该地址只能赋给指向 const 的指针。

对于类类型的 const 动态对象,如果该类提供了默认的构造函数,则此对象可隐式初始化:

     // allocate default initialized const empty string
     const string *pcs = new const string;

new 表达式没有显式初始化 pcs 所指向的对象,而是隐式地将 pcs 所指向的对象初始化为空的string 对象。

内置类型对象或未提供默认构造函数的类类型对象必须显式初始化。


警告:动态内存的管理容易出错

下面三种常见的程序错误都与动态内存分配相关:

  1. 删除( delete )指向动态分配内存的指针失败,因而无法将该块内存返还给自由存储区。

    删除动态分配内存失败称为“内存泄漏(memory leak)”。

    内存泄漏很难发现,一般需等应用程序运行了一段时间后,耗尽了所有内存空间时,内存泄漏才会显露出来。

  2. 读写已删除的对象。如果删除指针所指向的对象之后,将指针置为 0 值,则比较容易检测出这类错误。

  3. 对同一个内存空间使用两次 delete 表达式。当两个指针指向同一个动态创建的对象,删除时就会发生错误。

    如果在其中一个指针上做 delete 运算,将该对象的内存空间返还给自由存储区,然后接着delete 第二个指针,此时则自由存储区可能会被破坏。

    操纵动态分配的内存时,很容易发生上述错误,但这些错误却难以跟踪和修正。



    删除 const 对象

    尽管程序员不能改变 const 对象的值,但可撤销对象本身。如同其他动态对象一样, const 动态对象也是使用删除指针来释放的:

         delete pci; // ok: deletes a const object
    

    即使 delete 表达式的操作数是指向 intconst 对象的指针,该语句同样有效地回收pci 所指向的内容。


5.12. 类型转换

表达式是否合法取决于操作数的类型,而且合法的表达式其含义也由其操作数类型决定。

但是,在 C++ 中,某些类型之间存在相关的依赖关系。若两种类型相关,则可在需要某种类型的操作数位置上,使用该类型的相关类型对象或值。

如果两个类型之间可以相互转换,则称这两个类型相关。

考虑下列例子:

     int ival = 0;
     ival = 3.541 + 3; // typically compiles with a warning
ival 的值为 6。

首先做加法操作,其操作数是两个不同类型的值:3.541double 型的字面值常量,而 3 则是 int 型的字面值常量。C++ 并不是把两个不同类型的值直接加在一起,而是提供了一组转换规则,以便在执行算术操作之前,将两个操作数转换为同一种数据类型。

这些转换规则由编译器自动执行,无需程序员介入——有时甚至不需要程序员了解。因此,它们也被称为隐式类型转换

C++ 定义了算术类型之间的内置转换以尽可能防止精度损失。

通常,如果表达式的操作数分别为整型和浮点型,则整型的操作数被转换为浮点型。本例中,整数3被转换为double 类型,

然后执行浮点类型的加法操作,得 double 类型的结果6.541


下一步是将 double 类型的值赋给 int 型变量 ival。在赋值操作中,因为不可能更改左操作数对象的类型,因此左操作数的类型占主导地位。

如果赋值操作的左右操作数类型不相同,则右操作数会被转换为左边的类型。本例中,double 型的加法结果转换为int 型。doubleint 的转换自动按截尾形式进行,小数部分被舍弃。

于是 6.541 变成 6,然后赋给 ival。因为从 doubleint 的转换会导致精度损失,因此大多数编译器会给出警告。

例如,本书所用的测试例程的编译器给出如下警告:

     warning: assignment to 'int' from 'double'
为了理解 隐式类型转换,我们需要知道它们在什么时候发生,以及可能出现什么类型的转换。

何时发生隐式类型转换

编译器在必要时将类型转换规则应用到内置类型和类类型的对象上。在下列情况下,将发生隐式类型转换:

  • 在混合类型的表达式中,其操作数被转换为相同的类型:

         int ival;
         double dval;
         ival >= dval // ival converted to double
    


  • 用作条件的表达式被转换为 bool 类型:

         int ival;
         if (ival)   // ival converted to bool
         while (cin) // cin converted to bool
    条件操作符(?:)中的第一个操作数以及逻辑非(!)、逻辑与(&&)和逻辑或(||)的操作数都是条件表达式。出现在 ifwhilefordo while 语句中的同样也是条件表达式。

  • 用一表达式初始化某个变量,或将一表达式赋值给某个变量,则该表达式被转换为该变量的类型:

         int ival = 3.14; // 3.14 converted to int
         int *ip;
         ip = 0; // the int 0 converted to a null pointer of type int *
    
算术转换

C++ 语言为内置类型提供了一组转换规则,其中最常用的是算术转换

算术转换保证在执行操作之前,将二元操作符(如算术或逻辑操作符)的两个操作数转换为同一类型,并使表达式的值也具有相同的类型。

算术转换规则定义了一个类型转换层次,该层次规定了操作数应按什么次序转换为表达式中最宽的类型。

在包含多种类型的表达式中,转换规则要确保计算值的精度。例如,如果一个操作数的类型是long double,则无论另一个操作数是什么类型,都将被转换为 long double

最简单的转换为整型提升:对于所有比int 小的整型,包括 charsigned charunsigned charshortunsigned short

如果该类型的所有可能的值都能包容在 int 内,它们就会被提升为 int 型,否则,它们将被提升为unsigned int

如果将 bool 值提升为 int ,则 false 转换为 0,而 true 则转换为 1。

有符号与无符号类型之间的转换

若表达式中使用了无符号( unsigned )数值,所定义的转换规则需保护操作数的精度。unsigned 操作数的转换依赖于机器中整型的相对大小,因此,这类转换本质上依赖于机器。

包含 shortint 类型的表达式, short 类型的值转换为int

如果 int 型足够表示所有 unsigned short 型的值,则将 unsigned short 转换为int,否则,将两个操作数均转换为 unsigned int

例如,如果 short 用半字表示而 int 用一个字表示,则所有 unsigned 值都能包容在int 内,在这种机器上, unsigned short 转换为 int

longunsigned int 的转换也是一样的。

只要机器上的 long 型足够表示 unsigned int 型的所有值,就将 unsigned int 转换为 long 型,否则,将两个操作数均转换为unsigned long

在 32 位的机器上,longint 型通常用一个字长表示,因此当表达式包含 unsigned intlong 两种类型,其操作数都应转换为 unsigned long 型。

对于包含 signedunsigned int 型的表达式,其转换可能出乎我们的意料。表达式中的signed 型数值会被转换为 unsigned 型。

理解算术转换

研究大量例题是帮助理解算术转换的最好方法。下面大部分例题中,要么是将操作数转换为表达式中的最大类型,要么是在赋值表达式中将右操作数转换为左操作数的类型。

     bool      flag;         char           cval;
     short     sval;         unsigned short usval;
     int       ival;         unsigned int   uival;
     long      lval;         unsigned long  ulval;
     float     fval;         double         dval;
     3.14159L + 'a'; // promote 'a' to int, then convert to long double
     dval + ival;    // ival converted to double
     dval + fval;    // fval converted to double
     ival = dval;    // dval converted (by truncation) to int
     flag = dval;    // if dval is 0, then flag is false, otherwise true
     cval + fval;    // cval promoted to int, that int converted to float
     sval + cval;    // sval and cval promoted to int
     cval + lval;    // cval converted to long
     ival + ulval;   // ival converted to unsigned long
     usval + ival;   // promotion depends on size of unsigned short and int
     uival + lval;   // conversion depends on size of unsigned int and long

第一个加法操作的小写字母 'a' 是一个 char 类型的字符常量,正如我们在第 2.1.1 节介绍的,它是一个数值。

字母 'a' 表示的数值取决于机器字符集。

在 ASCII 机器中,字母 'a' 的值为 97。将 'a'long double 型数据相加时,char 型的值被提升为int 型,然后将 int 型转换为 long double 型,转换后的值再与 long double 型字面值相加。

另一个有趣的现象是最后两个表达式都包含 unsigned 数值。

5.12.3. 其他隐式转换
指针转换

在使用数组时,大多数情况下数组都会自动转换为指向第一个元素的指针:

     int ia[10];    // array of 10 ints
     int* ip = ia;  // convert ia to pointer to first element
不将数组转换为指针的例外情况有:数组用作取地址( &)操作符的操作数或 sizeof 操作符的操作数时,或用数组对数组的引用进行初始化时,不会将数组转换为指针。我们将在 第 7.2.4 节学习如何定义指向数组的引用(或指针)。

C++ 还提供了另外两种指针转换:指向任意数据类型的指针都可转换为 void* 类型;整型数值常量 0 可转换为任意指针类型。

Conversions to bool
转换为 bool 类型

算术值和指针值都可以转换为 bool 类型。如果指针或算术值为 0,则其 bool 值为false ,而其他值则为 true

     if (cp) /* ... */     // true if cp is not zero
     while (*cp) /* ... */ // dereference cp and convert resulting char to bool
这里, if 语句将 cp 的非零值转换为 truewhile 语句则对 cp 进行解引用,操作结果产生一个 char 型的值。

空字符( null )具有 0 值,被转换为 false,而其他字符值则转换为true

算术类型与 bool 类型的转换

可将算术对象转换为 bool 类型,bool 对象也可转换为 int 型。

将算术类型转换为 bool 型时,零转换为 false ,而其他值则转换为 true 。将bool 对象转换为算术类型时,true 变成 1,而 false 则为 0:

     bool b = true;
     int ival = b;   // ival == 1
     double pi = 3.14;
     bool b2 = pi;   // b2 is true
     pi = false;     // pi == 0
转换与枚举类型

C++ 自动将枚举类型的对象或枚举成员( enumerator )转换为整型,其转换结果可用于任何要求使用整数值的地方。例如,用于算术表达式:

     // point2d is 2, point2w is 3, point3d is 3, point3w is 4
     enum Points { point2d = 2, point2w,
                   point3d = 3, point3w };
     const size_t array_size = 1024;
     // ok: pt2w promoted to int
     int chunk_size = array_size * pt2w;
     int array_3d = array_size * point3d;

enum 对象或枚举成员提升为什么类型由机器定义,并且依赖于枚举成员的最大值。

无论其最大值是什么, enum 对象或枚举成员至少提升为 int 型。

如果 int 型无法表示枚举成员的最大值,则提升到能表示所有枚举成员值的、大于 int 型的最小类型(unsigned intlongunsigned long)。

转换为 const 对象

当使用非 const 对象初始化 const 对象的引用时,系统将非 const 对象转换为const 对象。

此外,还可以将非 const 对象的地址(或非 const 指针)转换为指向相关 const 类型的指针:

     int i;
     const int ci = 0;
     const int &j = i;   // ok: convert non-const to reference to const int
     const int *p = &ci; // ok: convert address of non-const to address of a const

由标准库类型定义的转换

类类型可以定义由编译器自动执行的类型转换。

迄今为止,我们使用过的标准库类型中,有一个重要的类型转换。从istream 中读取数据,并将此表达式作为 while 循环条件:

     string s;
     while (cin >> s)

这里隐式使用了 IO 标准库定义的类型转换。在与此类似的条件中,求解表达式cin >> s,即读 cin。无论读入是否成功,该表达式的结果都是 cin

while 循环条件应为 bool 类型的值,但此时给出的却是 istream 类类型的值,于是 istream 类型的值应转换为 bool 类型。

istream 类型转换为 bool 类型意味着要检验流的状态。

如果最后一次读 cin 的尝试是成功的,则流的状态将导致上述类型转换为 bool 类型后获得true 值——while 循环条件成立。

如果最后一次尝试失败,比如说已经读到文件尾了,此时将 istream 类型转换为 bool 类型后得falsewhile 循环条件不成立。

5.12.4. 显式转换

显式转换也称为强制类型转换(cast),包括以下列名字命名的强制类型转换操作符:static_castdynamic_castconst_castreinterpret_cast

<Beware>:

虽然有时候确实需要强制类型转换,但是它们本质上是非常危险的。

何时需要强制类型转换

因为要覆盖通常的标准转换,所以需显式使用强制类型转换。下面的复合赋值:

     double dval;
     int ival;
     ival *= dval; // ival = ival * dval

为了与 dval 做乘法操作,需将 ival 转换为 double 型,然后将乘法操作的double 型结果截尾为 int 型,再赋值给 ival

为了去掉将 ival 转换为 double 型这个不必要的转换,可通过如下强制将 dval 转换为 int 型:

     ival *= static_cast<int>(dval); // converts dval to int
显式使用强制类型转换的另一个原因是:可能存在多种转换时,需要选择一种特定的类型转换。

命名的强制类型转换

命名的强制类型转换符号的一般形式如下:

     cast-name<type>(expression);
其中 cast-namestatic_castdynamic_castconst_castreinterpret_cast 之一, type 为转换的目标类型,而 expression 则是被强制转换的值。

强制转换的类型指定了在 expression 上执行某种特定类型的转换。

dynamic_cast
dynamic_cast 支持运行时识别指针或引用所指向的对象。

const_cast

const_cast ,顾名思义,将转换掉表达式的 const 性质。例如,假设有函数 string_copy,只有唯一的参数,为 char* 类型,

我们对该函数只读不写。在访问该函数时,最好的选择是修改它让它接受 const char* 类型的参数。

如果不行,可通过 const_cast 用一个 const 值调用 string_copy 函数:

     const char *pc_str;
     char *pc = string_copy(const_cast<char*>(pc_str));

只有使用 const_cast 才能将 const 性质转换掉。在这种情况下,试图使用其他三种形式的强制转换都会导致编译时的错误。

类似地,除了添加或删除 const 特性,用 const_cast 符来执行其他任何类型转换,都会引起编译错误。

static_cast

编译器隐式执行的任何类型转换都可以由 static_cast 显式完成:

     double d = 97.0;
     // cast specified to indicate that the conversion is intentional
     char ch = static_cast<char>(d);
当需要将一个较大的算术类型赋值给较小的类型时,使用强制转换非常有用。

此时,强制类型转换告诉程序的读者和编译器:我们知道并且不关心潜在的精度损失。

对于从一个较大的算术类型到一个较小类型的赋值,编译器通常会产生警告。

当我们显式地提供强制类型转换时,警告信息就会被关闭。

如果编译器不提供自动转换,使用 static_cast 来执行类型转换也是很有用的。

例如,下面的程序使用 static_cast 找回存放在 void* 指针中的值:

     void* p = &d; // ok: address of any data object can be stored in a void*
     // ok: converts void* back to the original pointer type
     double *dp = static_cast<double*>(p);

可通过 static_cast 将存放在 void* 中的指针值强制转换为原来的指针类型,此时我们应确保保持指针值。

也就是说,强制转换的结果应与原来的地址值相等。

reinterpret_cast
reinterpret_cast 通常为操作数的位模式提供较低层次的重新解释。

<Beware>:

reinterpret_cast 本质上依赖于机器。为了安全地使用 reinterpret_cast,要求程序员完全理解所涉及的数据类型,以及编译器实现强制类型转换的细节。


如,对于下面的强制转换:

     int *ip;
     char *pc = reinterpret_cast<char*>(ip);

程序员必须永远记得 pc 所指向的真实对象其实是 int 型,而并非字符数组。

任何假设 pc 是普通字符指针的应用,都有可能带来有趣的运行时错误。例如,下面语句用 pc 来初始化一个string 对象:

     string str(pc);
它可能会引起运行时的怪异行为。

pc 初始化 str 这个例子很好地说明了显式强制转换是多么的危险。

问题源于类型已经改变时编译器没有提供任何警告或错误提示。

当我们用 int 型地址初始化 pc 时,由于显式地声明了这样的转换是正确的,因此编译器不提供任何错误或警告信息。

后面对 pc 的使用都假设它存放的是 char* 型对象的地址,编译器确实无法知道 pc 实际上是指向 int 型对象的指针。

因此用 pc 初始化 str 是完全正确的——虽然实际上是无意义的或是错误的。

查找这类问题的原因相当困难,特别是如果 ippc 的强制转换和使用 pc 初始化string 对象这两个应用发生在不同文件中的时候。

建议:避免使用强制类型转换

强制类型转换关闭或挂起了正常的类型检查。强烈建议程序员避免使用强制类型转换,不依赖强制类型转换也能写出很好的 C++ 程序。

这个建议在如何看待 reinterpret_cast 的使用时非常重要。此类强制转换总是非常危险的。

相似地,使用 const_cast 也总是预示着设计缺陷。设计合理的系统应不需要使用强制转换抛弃const 特性。

其他的强制转换,如 static_cast dynamic_cast,各有各的用途,但都不应频繁使用。

每次使用强制转换前,程序员应该仔细考虑是否还有其他不同的方法可以达到同一目的。

如果非强制转换不可,则应限制强制转换值的作用域,并且记录所有假定涉及的类型,这样能减少错误发生的机会。


旧式强制类型转换

在引入命名的强制类型转换操作符之前,显式强制转换用圆括号将类型括起来实现:

     char *pc = (char*) ip;
效果与使用 reinterpret_cast 符号相同,但这种强制转换的可视性比较差,难以跟踪错误的转换。

标准 C++ 为了加强类型转换的可视性,引入命名的强制转换操作符,为程序员在必须使用强制转换时提供了更好的工具。

例如,非指针的 static_castconst_cast 要比 reinterpret_cast 更安全。

结果使程序员(以及读者和操纵程序的工具)可清楚地辨别代码中每个显式的强制转换潜在的风险级别。

虽然标准 C++ 仍然支持旧式强制转换符号,但是我们建议,只有在 C 语言或标准 C++ 之前的编译器上编写代码时,才使用这种语法。

旧式强制转换符号有下列两种形式:

     type (expr); // Function-style cast notation
     (type) expr; // C-language-style cast notation

旧式强制转换依赖于所涉及的数据类型,具有与 const_caststatic_castreinterpret_cast 一样的行为。

在合法使用 static_castconst_cast 的地方,旧式强制转换提供了与各自对应的命名强制转换一样的功能。

如果这两种强制转换均不合法,则旧式强制转换执行 reinterpret_cast 功能。例如,我们可用旧式符号重写上一节的强制转换:

     int ival; double dval;
     ival += int (dval); // static_cast: converts double to int
     const char* pc_str;
     string_copy((char*)pc_str); // const_cast: casts away const
     int *ip;
     char *pc = (char*)ip; // reinterpret_cast: treats int* as char*

支持旧式强制转换符号是为了对“在标准 C++ 之前编写的程序”保持向后兼容性,并保持与 C 语言的兼容性。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值