从零开始学C++(第四讲:赋值操作符)
本篇是《C++根据零开始(二)》的延续,说明《C++根据零开始(二)》中遗留下来的对于表达式的内容,并为下篇指针的运用做一点铺垫。虽然上篇可以说明了变量是什么,但对于变量最主要的东西却因为篇幅限制而无说明,下面先说明如何访问内存。
赋值语句
前面可以说明,要访问内存,就就得相应的地址以表明访问哪块内存,而变量是一个映射,所以变量名就相当于一个地址。对于内存的操作,在那么情况下就只有读取内存中的数值和用数值写入内存(不考虑分配和释放内存),在C++中,为了用一数值写入某变量对应的地址所标识的内存中(出于简便,将来称变量a对应的地址为变量a的地址,而直接称变量a的地址所标识的内存为变量a),只需先书写变量名,后接“=”,再接欲写入的数字(对于数字,请参考《C++根据零开始(二)》)还有分号。如下
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++就完了,为了促使您相当地重视前面的红字,下面用介绍更令人火大的东西,可能您可以同意上面的红字,则下面这一节完全可以跳过,其对编程来讲可以认为根本无每一个意义(要不可能为了写这篇文章,我总不知为什么它的存在)。