当前位置导航:炫浪网>>网络学院>>编程开发>>C++教程>>C++进阶与实例

C++程序设计从零开始之赋值操作符

    赋值语句

    前面已经说明,要访问内存,就需要相应的地址以表明访问哪块内存,而变量是一个映射,因此变量名就相当于一个地址。对于内存的操作,在一般情况下就只有读取内存中的数值和将数值写入内存(不考虑分配和释放内存),在C++中,为了将一数值写入某变量对应的地址所标识的内存中(出于简便,以后称变量a对应的地址为变量a的地址,而直接称变量a的地址所标识的内存为变量a),只需先书写变量名,后接“=”,再接欲写入的数字以及分号。如下:

    a = 10.0f; b = 34;

    由于接的是数字,因此就可以接表达式并由编译器生成计算相应表达式所需的代码,也就可如下:

    c = a / b * 120.4f;

    上句编译器将会生成进行除法和乘法计算的CPU指令,在计算完毕后(也就是求得表达式a / b * 120.4f的值了后),也会同时生成将计算结果放到变量c中去的CPU指令,这就是语句的基本作用(对于语句,在《C++从零开始(六)》中会详细说明)。

    上面在书写赋值语句时,应该确保此语句之前已经将使用到的变量定义过,这样编译器才能在生成赋值用的CPU指令时查找到相应变量的地址,进而完成CPU指令的生成。如上面的a和b,就需要在书写上面语句前先书写类似下面的变量定义:

    float a; long b;

    直接书写变量名也是一条语句,其导致编译器生成一条读取相应变量的内容的语句。即可以如下书写:

    a;

    上面将生成一条读取内存的语句,即使从内存中读出来的数字没有任何应用(当然,如果编译器开了优化选项,则上面的语句将不会生成任何代码)。从这一点以及上面的c = a / b * 120.4f;语句中,都可以看出一点——变量是可以返回数字的。而变量返回的数字就是按照变量的类型来解释变量对应内存中的内容所得到的数字。这句话也许不是那么容易理解,在看过后面的类型转换一节后应该就可以理解了。

    因此为了将数据写入一块内存,使用赋值语句(即等号);要读取一块内存,书写标识内存的变量名。所以就可以这样书写:a = a + 3;假设a原来的值为1,则上面的赋值语句将a的值取出来,加上3,得到结果4,将4再写入a中去。由于C++使用“=”来代表赋值语句,很容易使人和数学中的等号混淆起来,这点应注意。

    而如上的float a;语句,当还未对变量进行任何赋值操作时,a的值是什么?上帝才知道。当时的a的内容是什么(对于VC编译器,在开启了调试选项时,将会用0xCCCCCCCC填充这些未初始化内存),就用IEEE的real*4格式来解释它并得到相应的一个数字,也就是a的值。因此应在变量定义的时候就进行赋值(但是会有性能上的影响,不过很小),以初始化变量而防止出现莫名其妙的值,如:float a = 0.0f;。

    赋值操作符

    上面的a = a + 3;的意思就是让a的值增加3.在C++中,对于这种情况给出了一种简写方案,即前面的语句可以写成:a += 3;。应当注意这两条语句从逻辑上讲都是使变量a的值增3,但是它们实际是有区别的,后者可以被编译成优化的代码,因为其意思是使某一块内存的值增加一定数量,而前者是将一个数字写入到某块内存中。所以如果可能,应尽量使用后者,即a += 3;。这种语句可以让编译器进行一定的优化(但由于现在的编译器都非常智能,能够发现a = a + 3;是对一块内存的增值操作而不是一块内存的赋值操作,因此上面两条语句实际上可以认为完全相同,仅仅只具有简写的功能了)。

    对于上面的情况,也可以应用在减法、乘法等二元非逻辑操作符(不是逻辑值操作符,即不能a &&= 3;)上,如:a *= 3; a -= 4; a |= 34; a >>= 3;等。

    除了上面的简写外,C++还提供了一种简写方式,即a++;,其逻辑上等同于a += 1;。同上,在电脑编程中,加一和减一是经常用到的,因此CPU专门提供了两条指令来进行加一和减一操作(转成汇编语言就是Inc和Dec),但速度比直接通过加法或减法指令来执行要快得多。为此C++中也就提供了“++”和“—”操作符来对应Inc和Dec.所以a++;虽然逻辑上和a = a + 1;等效,实际由于编译器可能做出的优化处理而不同,但还是如上,由于编译器的智能化,其是有可能看出a = a + 1;可以编译成Inc指令进而即使没有使用a++;却也依然可以得到优化的代码,这样a++;将只剩下简写的意义而已。

    应当注意一点,a = 3;这句语句也将返回一个数字,也就是在a被赋完值后a的值。由于其可以返回数字,按照《C++从零开始(二)》中所说,“=”就属于操作符,也就可以如下书写:

    c = 4 + ( a = 3 );

    之所以打括号是因为“=”的优先级较“+”低,而更常见和正常的应用是:c = a = 3;

    应该注意上面并不是将c和a赋值为3,而是在a被赋值为3后再将a赋值给c,虽然最后结果和c、a都赋值为3是一样的,但不应该这样理解。由于a++;表示的就是a += 1;就是a = a + 1;,因此a++;也将返回一个数字。也由于这个原因,C++又提供了另一个简写方式,++a;。

    假设a为1,则a++;将先返回a的值,1,然后再将a的值加一;而++a;先将a的值加一,再返回a的值,2.而a—和—a也是如此,只不过是减一罢了。

    上面的变量a按照最上面的变量定义,是float类型的变量,对它使用++操作符并不能得到预想的优化,因为float类型是浮点类型,其是使用IEEE的real*4格式来表示数字的,而不是二进制原码或补码,而前面提到的Inc和Dec指令都是出于二进制的表示优点来进行快速增一和减一,所以如果对浮点类型的变量运用“++”操作符,将完全只是简写,没有任何的优化效果(当然,如果CPU提供了新的指令集,如MMX等,以对real*4格式进行快速增一和减一操作,且编译器支持相应指令集,则还是可以产生优化效果的)。

    赋值操作符的返回值

    在进一步了解++a和a++的区别前,先来了解何谓操作符的计算(Evaluate)。操作符就是将给定的数字做一些处理,然后返回一个数字。而操作符的计算也就是执行操作符的处理,并返回值。前面已经知道,操作符是个符号,其一侧或两侧都可以接数字,也就是再接其他操作符,而又由于赋值操作符也属于一种操作符,因此操作符的执行顺序变得相当重要。

    对于a + b + c,将先执行a + b,再执行( a + b ) + c的操作。你可能觉得没什么,那么如下,假设a之前为1:

    c = ( a *= 2 ) + ( a += 3 );

    上句执行后a为5.而c = ( a += 3 ) + ( a *= 2 );执行后,a就是8了。那么c呢?结果可能会大大的出乎你的意料。前者的c为10,而后者的c为16.

    上面其实是一个障眼法,其中的“+”没有任何意义,即之所以会从左向右执行并不是因为“+”的缘故,而是因为( a *= 2 )和( a += 3 )的优先级相同,而按照“()”的计算顺序,是从左向右来计算的。但为什么c的值不是预想的2 + 5和4 + 8呢?因为赋值操作符的返回值的关系。

    赋值操作符返回的数字不是变量的值,而是变量对应的地址。这很重要。前面说过,光写一个变量名就会返回相应变量的值,那是因为变量是一个映射,变量名就等同于一个地址。C++中将数字看作一个很特殊的操作符,即任何一个数字都是一个操作符。而地址就和长整型、单精度浮点数这类一样,是数字的一种类型。当一个数字是地址类型时,作为操作符,其没有要操作的数字,仅仅返回将此数字看作地址而标识的内存中的内容(用这个地址的类型来解释)。地址可以通过多种途径得到,如上面光写一个变量名就可以得到其对应的地址,而得到的地址的类型也就是相应的变量的类型。如果这句话不能理解,在看过下面的类型转换一节后应该就能了解了。

    所以前面的c = ( a += 3 ) + ( a *= 2 );,由于“()”的参与改变了优先级而先执行了两个赋值操作符,然后两个赋值操作符都返回a的地址,然后计算“+”的值,分别计算两边的数字——a的地址(a的地址也是一个操作符),也就是已经执行过两次赋值操作的a的值,得8,故最后的c为16.而另一个也由于同样的原因使得c为10.

    现在考虑操作符的计算顺序。当同时出现了几个优先级相同的操作符时,不同的操作符具有不同的计算顺序。前面的“()”以及“-”、“*”等这类二元操作符的计算顺序都是从左向右计算,而“!”、负号“-”等前面介绍过的一元操作符都是从右向左计算的,如:!-!!a;,假设a为3.先计算从左朝右数第三个“!”的值,导致计算a的地址的值,得3;然后逻辑取反得0,接着再计算第二个“!”的值,逻辑取反后得1,再计算负号“-”的值,得-1,最后计算第一个“!”的值,得0.

    赋值操作符都是从右向左计算的,除了后缀“++”和后缀“—”(即上面的a++和a——)。因此上面的c = a = 3;,因为两个“=”优先级相同,从右向左计算,先计算a = 3的值,返回a对应的地址,然后计算返回的地址而得到值3,再计算c = ( a = 3 ),将3写入c.而不是从左向右计算,即先计算c = a,返回c的地址,然后再计算第二个“=”,将3写入c,这样a就没有被赋值而出现问题。又:

    a = 1; c = 2; c *= a += 4;

    由于“*=”和“+=”的优先级相同,从右向左计算先计算a += 4,得a为5,然后返回a的地址,再计算a的地址得a的值5,计算“*=”以使得c的值为10.

    因此按照前面所说,++a将返回a的地址,而a++也因为是赋值操作符而必须返回一个地址,但很明显地不能是a的地址了,因此编译器将编写代码以从栈中分配一块和a同样大小的内存,并将a的值复制到这块临时内存中,然后返回这块临时内存的地址。由于这块临时内存是因为编译器的需要而分配的,与程序员完全没有关系,因此程序员是不应该也不能写这块临时内存的(因为编译器负责编译代码,如果程序员欲访问这块内存,编译器将报错),但可以读取它的值,这也是返回地址的主要目的。所以如下的语句没有问题:

    ( ++a ) = a += 34;

    但( a++ ) = a += 34;就会在编译时报错,因为a++返回的地址所标识的内存只能由编译器负责处理,程序员只能获得其值而已。

    a++的意思是先返回a的值,也就是上面说的临时内存的地址,然后再将变量的值加一。如果同时出现多个a++,那么每个a++都需要分配一块临时内存(注意前面c = ( a += 3 ) + ( a *= 2 );的说明),那么将有点糟糕,而且a++的意思是先返回a的值,那么到底是什么时候的a的值呢?在VC中,当表达式中出现后缀“++”或后缀“—”时,只分配一块临时内存,然后所有的后缀“++”或后缀“—”都返回这个临时内存的地址,然后在所有的可以计算的其他操作符的值计算完毕后,再将对应变量的值写入到临时内存中,计算表达式的值,最后将对应变量的值加一或减一。

    因此:a = 1; c = ( a++ ) + ( a++ );执行后,c的值为2,而a的值为3.而如下:

    a = 1; b = 1; c = ( ++a ) + ( a++ ) + ( b *= a++ ) + ( a *= 2 ) + ( a *= a++ );

    执行时,先分配临时内存,然后由于5个“()”,其计算顺序是从左向右,

    计算++a的值,返回增一后的a的地址,a的值为2

    计算a++的值,返回临时内存的地址,a的值仍为2

    计算b *= a++中的a++,返回临时内存的地址,a的值仍为2

    计算b *= a++中的“*=”,将a的值写入临时内存,计算得b的值为2,返回b的地址

    计算a *= 2的值,返回a的地址,a的值为4

    计算a *= a++中的a++,返回临时内存的地址,a的值仍为4

    计算a *= a++中的“*=”,将a的值写入临时内存,返回a的地址,a的值为16

    计算剩下的“+”,为了进行计算,将a的值写入临时内存,得值16 + 16 + 2 + 16 + 16为66,写入c中

    计算三个a++欠下的加一,a最后变为19.

    上面说了那么多,无非只是想告诫你——在表达式中运用赋值操作符是不被推崇的。因为其不符合平常的数学表达式的习惯,且计算顺序很容易搞混。如果有多个“++”操作符,最好还是将表达式分开,否则很容易导致错误的计算顺序而计算错误。并且导致计算顺序混乱的还不止上面的a++就完了,为了让你更加地重视前面的红字,下面将介绍更令人火大的东西,如果你已经同意上面的红字,则下面这一节完全可以跳过,其对编程来讲可以认为根本没有任何意义(要不是为了写这篇文章,我都不知道它的存在)。

    序列点(Sequence Point)和附加效果(Side Effect)

    在计算c = a++时,当c的值计算(Evaluate)出来时,a的值也增加了一,a的值加一就是计算前面表达式的附加效果。有什么问题?它可能影响表达式的计算结果。

    对于a = 0; b = 1; ( a *= 2 ) && ( b += 2 );,由于两个“()”优先级相同,从左向右计算,计算“*=”而返回a的地址,再计算“+=”而返回b的地址,最后由于a的值为0而返回逻辑假。很正常,但效率低了点。

    如果“&&”左边的数字已经是0了,则不再需要计算右边的式子。同样,如果“||”左边的数字已经非零了,也不需要再计算右边的数字。因为“&&”和“||”都是数学上的,数学上不管先计算加号左边的值还是右边的值,结果都不会改变,因此“&&”和“||”才会做刚才的解释。这也是C++保证的,既满足数学的定义,又能提供优化的途径(“&&”和“||”右边的数字不用计算了)。

    因此上面的式子就会被解释成——如果a在自乘了2后的值为0,则b就不用再自增2了。这很明显地违背了我们的初衷,认为b无论如何都会被自增2的。但是C++却这样保证,不仅仅是因为数学的定义,还由于代码生成的优化。但是按照操作符的优先级进行计算,上面的b += 2依旧会被执行的(这也正是我们会书写上面代码的原因)。为了实现当a为0时b += 2不会被计算,C++提出了序列点的概念。

    序列点是一些特殊位置,由C++强行定义(C++并未给出序列点的定义,因此不同的编译器可能给出不同的序列点定义,VC是按照C语言定义的序列点)。当在进行操作符的计算时,如果遇到序列点,则序列点处的值必须被优先计算,以保证一些特殊用途,如上面的保证当a为0时不计算b += 2,并且序列点相关的操作符(如前面的“&&”和“||”)也将被计算完毕,然后才恢复正常的计算。

    “&&”的左边数字的计算就是一个序列点,而“||”的左边数字的计算也是。C++定义了多个序列点,包括条件语句、函数参数等条件下的表达式计算,在此,不需要具体了解有哪些序列点,只需要知道由于序列点的存在而可能导致赋值操作符的计算出乎意料。下面就来分析一个例子:

    a = 0; b = 1; ( a *= 2 ) && ( b += ++a );

    按照优先级的顺序,编译器发现要先计算a *= 2,再计算++a,接着“+=”,最后计算“&&”。然后编译器发现这个计算过程中,出现了“&&”左边的数字这个序列点,其要保证被优先计算,这样就有可能不用计算b += ++a了。所以编译器先计算“&&”的数字,通过上面的计算过程,编译器发现就要计算a *= 2才能得到“&&”左边的数字,因此将先计算a *= 2,返回a的地址,然后计算“&&”左边的数字,得a的值为0,因此就不计算b += ++a了。而不是最开始想象的由于优先级的关系先将a加一后再进行a的计算,以返回1.所以上面计算完毕后,a为0,b为1,返回0,表示逻辑假。

    因此序列点的出现是为了保证一些特殊规则的出现,如上面的“&&”和“||”。再考虑“,”操作符,其操作是计算两边的值,然后返回右边的数字,即:a, b + 3将返回b + 3的值,但是a依旧会被计算。由于“,”的优先级是最低的(但高于前面提到的“数字”操作符),因此如果a = 3, 4;,那么a将为3而不是4,因为先计算“=”,返回a的地址后再计算“,”。又:

    a = 1; b = 0; b = ( a += 2 ) + ( ( a *= 2, b = a - 1 ) && ( c = a ) );

    由于“&&”左边数字是一个序列点,因此先计算a *= 2, b的值,但根据“,”的返回值定义,其只返回右边的数字,因此不计算a *= 2而直接计算b = a – 1得0,“&&”就返回了,但是a *= 2就没有被计算而导致a的值依旧为1,这违背了“,”的定义。为了消除这一点(当然可能还有其他应用“,”的情况),C++也将“,”的左边数字定为了序列点,即一定会优先执行“,”左边的数字以保证“,”的定义——计算两边的数字。所以上面就由于“,”左边数字这个序列点而导致a *= 2被优先执行,并导致b为1,因此由于“&&”是序列点且其左边数字非零而必须计算完右边数字后才恢复正常优先级,而计算c = a,得2,最后才恢复正常优先级顺序,执行a += 2和“+”。结果就a为4,c为2,b为5.

    所以前面的a = 3, 4;其实就应该是编译器先发现“,”这个序列点,而发现要计算“,”左边的值,必须先计算出a = 3,因此才先计算a = 3以至于感觉序列点好像没有发生作用。下面的式子请自行分析,执行后a为4,但如果将其中的“,”换成“&&”,a为2.

    a = 1; b = ( a *= 2 ) + ( ( a *= 3 ), ( a -= 2 ) );

    如果上面你看得很晕,没关系,因为上面的内容根本可以认为毫无意义,写在这里也只是为了进一步向你证明,在表达式中运用赋值运算符是不好的,即使它可能让你写出看起来简练的语句,但它也使代码的可维护性降低。

    类型转换

    数字可以是浮点数或是整型数或其他,也就是说数字是具有类型的。注意《C++从零开始(三)》中对类型的解释,类型只是说明如何解释状态,而在前面已经说过,出于方便,使用二进制数来表示状态,因此可以说类型是用于告诉编译器如何解释二进制数的。

    所以,一个长整型数字是告诉编译器将得到的二进制数表示的状态按照二进制补码的格式来解释以得到一个数值,而一个单精度浮点数就是告诉编译器将得到的二进制数表示的状态按照IEEE的real*4的格式来解释以得到一个是小数的数值。很明显,同样的二进制数表示的状态,按照不同的类型进行解释将得到不同的数值,那么编译器如何知道应该使用什么类型来进行二进制数的解释?

    前面已经说过,数字是一种很特殊的操作符,其没有操作数,仅仅返回由其类型而定的二进制数表示的状态(以后为了方便,将“二进制数表示的状态”称作“二进制数”)。而操作符就是执行指令并返回数字,因此所有的操作符到最后一定执行的是返回一个二进制数。这点很重要,对于后面指针的理解有着重要的意义。

    先看15;,这是一条语句,因为15是一个数字。所以15被认为是char类型的数字(因为其小于128,没超出char的表示范围),将返回一个8位长的二进制数,此二进制数按照补码格式编写,为00001111.

    再看15.0f,同上,其由于接了“f”这个后缀而被认为是float类型的数字,将返回一个32位长的二进制数,此二进制数按照IEEE的real*4格式编写,为1000001011100000000000000000000.

    虽然上面15和15.0f的数值相等,但由于是不同的类型导致了使用不同的格式来表示,甚至连表示用的二进制数的长度都不相同。因此如果书写15.0f == 15;将返回0,表示逻辑假。但实际却返回1,为什么?

    上面既然15和15.0f被表示成完全不同的两个二进制数,但我们又认为15和15.0f是相等的,但它们的二进制表示不同,怎么办?将表示15.0f的二进制数用IEEE的real*4格式解释出15这个数值,然后再将其按8位二进制补码格式编写出二进制数,再与原来的表示15的二进制数比较。

    为了实现上面的操作,C++提供了类型转换操作符——“()”。其看起来和括号操作符一样,但是格式不同:(<类型名>)<数字>或<类型名>(<数字>)。

    上面类型转换操作符的<类型名>不是数字,因此其将不会被操作,而是作为一个参数来控制其如何操作后面的<数字>.<类型名>是一个标识符,其唯一标识一个类型,如char、float等。类型转换操作符的返回值就如其名字所示,将<数字>按照<类型名>标识的类型来解释,返回类型是<类型名>的数字。因此,上面的例子我们就需要如下编写:15 == ( char )15.0f;,现在其就可以返回1,表示逻辑真了。但是即使不写( char ),前面的语句也返回1.这是编译器出于方便的缘故而帮我们在15前添加了( float ),所以依然返回1.这被称作隐式类型转换,在后面说明类的时候,还将提到它。

    某个类型可以完全代替另一个类型时,编译器就会进行上面的隐式类型转换,自动添加类型转换操作符。如:char只能表示-128到127的整数,而float很明显地能够表示这些数字,因此编译器进行了隐式类型转换。应当注意,这个隐式转换是由操作符要求的,即前面的“==”要求两面的数字类型一致,结果发现两边不同,结果编译器将char转成float,然后再执行“==”的操作。注意:在这种情况下,编译器总是将较差的类型(如前面的char)转成较好的类型(如前面的float),以保证不会发生数值截断问题。如:-41 == 3543;,左边是char,右边是short,由于short相对于char来显得更优(short能完全替代char),故实际为:( short )-41 == 3543;,返回0.而如果是-41 == ( char )3543;,由于char不能表示3543,则3543以补码转成二进制数0000110111010111,然后取其低8位,而导致高8位的00001101被丢弃,此被称为截断。结果( char )3543的返回值就是类型为char的二进制数11010111,为-41,结果-41 == ( char )3543;的返回值将为1,表示逻辑真,很明显地错误。因此前面的15 == 15.0f;实际将为( float )15 == 15.0f;。

    注意前面之所以会朝好的方向发展(即char转成float),完全是因为“==”的缘故,其要求这么做。下面考虑“=”:short b = 3543; char a = b;。因为b的值是short类型,而“=”的要求就是一定要将“=”右边的数字转成和左边一样,这样才能进行正确的内存的写入(简单地将右边数字返回的二进制数复制到左边的地址所表示的内存中)。因此a将为-41.但是上面是编译器按照“=”的要求自行进行了隐式转换,可能是由于程序员的疏忽而没有发现这个错误(以为b的值一定在-128到127的范围内),因此编译器将对上面的情况给出一个警告,说b的值可能被截断。为了消除编译器的疑虑,如下:char a = ( char )b;。这样称为显示类型转换,其告诉编译器——“我知道可能发生数据截断,但是我保证不会截断”。因此编译器将不再发出警告。但是如下:char a = ( char )3543;,由于编译器可以肯定3543一定会被截断而导致错误的返回值,因此编译器将给出警告,说明3543将被截断,而不管前面的类型转换操作符是否存在。

    现在应该可以推出——15 + 15.0f;返回的是一个float类型的数字。因此如果如下:char a = 15 + 15.0f;,编译器将发出警告,说数据可能被截断。因此改成如下:char a = ( char )15 + 15.0f;,但类型转换操作符“()”的优先级比“+”高,结果就是15先被转换为char然后再由于“+”的要求而被隐式转成float,最后返回float给“=”而导致编译器依旧发出警告。为此,就需要提高“+”的优先级,如下:char a = ( char )( 15 + 15.0f );就没事了(或char( 15 + 15.0f )),其表示我保证15 + 15.0f不会导致数据截断。

    应该注意类型转换操作符“()”和前缀“++”、“!”、负号“-”等的优先级一样,并且是从右向左计算的,因此( char )-34;将会先计算-34的值,然后再计算( char )的值,这也正好符合人的习惯。

相关内容
赞助商链接