1. 程式人生 > >C語言++a與a++的實現機制與操作符結合優先順序

C語言++a與a++的實現機制與操作符結合優先順序

看到一道“經典Linux C“面試題,關於左值和右值的。

華為筆試題
1.寫出判斷ABCD四個表示式的是否正確, 若正確, 寫出經過表示式中 a的值(3分)
int a = 4;
(A)a += (a++); (B) a += (++a) ;(C) (a++) += a;(D) (++a) += (a++);
a = ?
答:C錯誤,左側不是一個有效變數,不能賦值,可改為(++a) += a;(補充:在我現在用的gcc中,++a也是不能當左值的)

改後答案依次為9,10,10,11

可以看出,這個題除了測試你關於++a與a++中“自加1是先生效還是後生效?”以外,還要測試你對左值和右值的理解。

根據這個參考答案大膽的猜測一下過程:

A選項,a加上自身的後自增(還沒有生效),得a的雙倍,隨後a的後自增生效,變成了2a+1,即9。

B選項,a加上自身的前自增。注意:這個自增已經生效了,因為是賦值語句,等號“=”右邊的表示式先生效(到底賦值表示式左邊右邊怎麼個生效順序?下文也驗證了,gcc把這個問題避免了,因為左邊不允許出現這種形式!)等號右邊的a變5,左邊的a隨即也變成了5,所以是兩個a的前自增(即4 + 1 == 5)相加(5 + 5),結果10!

C選項(“改”後),a的後自增加上a的自身,這裡因為後自增(a++)是個“臨時變數”,沒有記憶體地址(即右值),所以不能用左賦值目標,替換成“左值”(++a),根據B選項等號右邊先生效的原則,應該是4+4,之後再自加1,變成9才對(或者理解為4+1,再+4,反正沒區別)。。。。。反正順序不對,有衝突~!!

D選項,a的前自加1(值為5)加上a的後自加1(為便於理解,寫成5++),結果10,表示式結束後a的後自加1生效,結果11。

有些小衝突!如果賦值表示式的符號“=”左邊和右邊有先後順序(一般認為右邊先)的話,C就是錯的,因為你不應該改變等號右邊先執行的那部分~

除非說++a在整個賦值表示式之前就生效,而a++在整個表示式結束時才生效。這樣才能解釋通!!!

那麼,事實究竟如何?

還是做個程式測試了下的好,這種比較迷惑人的東西一定要自己親自操作一下,多試試條件,看看細小差別。

因為這四個選項是重複的,所以把a換成了a、b、c、d四個變數(這些自加賦值“表示式”一定不要放在printf裡,printf要單獨放,因為自加導致列印結果不準確。)

#include<stdio.h>
//some unique and different useage of plusplus
main(){
        int a = 4;
        int b = 4;
        int c = 4;
        int d = 4;

        a += (a++);
        b += (++b);
//who said that ++c could be work in linux C????
//      (c++) += c;
//      (++c) += c;
//      (++d) += d++;

        printf("a = %d\n",a);
        printf("b = %d\n",b);
        printf("c = %d\n",c);
        printf("d = %d\n",d);
}
gcc編譯結果:
aplusplus.c:12:8: error: lvalue required as left operand of assignment
aplusplus.c:13:8: error: lvalue required as left operand of assignment
aplusplus.c:14:8: error: lvalue required as left operand of assignment
這三行分別指註釋掉的三個語句~~
實測發現,(c++)不能當做左值,(++c)和(++d)同樣不行,和括號也沒有關係,那麼在我的 

gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3 
貌似測試不了++a作為左值的情況了。

這塊弄不了,先掛起,看看左值右值的問題吧,根據描述,右值一般是沒有記憶體地址的,是臨時的,通俗點說,是個表示式,不是個值。

用gdb設斷點,看下執行過程:

首先,a(值為0x4)和b(值為0x4)分別壓入棧,地址分別是0x10和0x14

4        int a = 4;
1: x/i $pc
=> 0x80483ed <main+9>:    movl   $0x4,0x10(%esp)
(gdb) si
5        int b = 4;
1: x/i $pc
=> 0x80483f5 <main+17>:    movl   $0x4,0x14(%esp)

Breakpoint 1, main () at aplusplus.c:9
9		a += (a++);
1: x/i $pc
=> 0x804840d <main+41>:	mov    0x10(%esp),%eax
第九行是a += (a++);處相應斷點,看下a和b的自加過程。
=> 0x804840d <main+41>:	mov    0x10(%esp),%eax
   0x8048411 <main+45>:	add    %eax,%eax
   0x8048413 <main+47>:	mov    %eax,0x10(%esp)
   0x8048417 <main+51>:	addl   $0x1,0x10(%esp)

   0x804841c <main+56>:	addl   $0x1,0x14(%esp)
   0x8048421 <main+61>:	mov    0x14(%esp),%eax
   0x8048425 <main+65>:	add    %eax,%eax
   0x8048427 <main+67>:	mov    %eax,0x14(%esp)
。。。。。。
先看a += a++;

a從棧地址0x10中移入eax暫存器中,

在eax暫存器中自加(相當於double了一下4*2 == 8),

從eax再移回棧地址0x10,

最後,給棧地址0x10中加入直接數1(8+1 == 9)


然後b += ++b;
先把直接數1加到b所在棧地址0x14中(4+1 == 5),

然後從棧中移動b(5)到eax暫存器中,

在eax暫存器中自加(5*2 == 10),

移動b回棧中地址0x14。

結論:不管邏輯上怎麼認為,什麼“++a為自加1先生效,a++為自加1後生效,臨時變數不可被賦值,等號左邊右邊誰先生效”。到最後,怎麼實現都是編譯器說了算,以下至少能算是我這個版本的 gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3 下的結論。


a++和++b,中間過程類似,都是在eax暫存器中,用自己加自己(即乘以2),主要區別就是自加1的位置,一個在最前,一個在最後。不知道其他版本的編譯器,至少我這個版本的編譯器把問題簡化了,根本不允許在賦值符號”=“左邊放++a或a++一類語句,也就是說++a也不被認為是左值,所以根本沒法區分賦值符號”=“左右的先後一說。

那麼,還有臨時變數一說麼?再看兩種情況:test++和d += a++(因為之前a和b都是和自己相加,這兩個情況沒測到)

8		int test = 5;
1: x/i $pc
=> 0x804840d <main+41>:	movl   $0x5,0x2c(%esp)
(gdb) 
9		test++;
1: x/i $pc
=> 0x8048415 <main+49>:	addl   $0x1,0x2c(%esp)直接把立即數加到test所佔的棧空間0x2c中
22		d += a++;
1: x/i $pc
=> 0x804848c <main+168>:	mov    0x1c(%esp),%eax     把a移到eax暫存器中,
=> 0x8048490 <main+172>:	add    %eax,0x28(%esp)     a的值直接加到d所在記憶體地址中(d += a)
=> 0x8048494 <main+176>:	addl   $0x1,0x1c(%esp)     將立即數1加給a

所謂臨時變數不臨時變數,至少從這個角度無法證明,尤其單獨的test++,直接在原地址修改,當然有地址,當然是左值(不足之處是現在的這個是巨集彙編,還不是單獨的彙編命令,不夠詳細)。只有在a += a++;之類更復雜的語句中才能體會這種差別來,所以,這應該是程式語言和編譯器之間協調的一個過程吧,編譯器看要怎麼來解決某種情況,怎麼實現,解決不了就禁止了。

如果真要我解釋:(a++)是一個“沒地址的臨時變數”的話,那根據上面的過程,我更願意相信這個“臨時變數”根本沒存在過

如果在暫存器eax中的值算臨時變數的話,那它其實還是原值,而不是自加1以後的值。

歸根結底,那是C語言的定義,“左值返回地址”、”右值是無地址的表示式“。一旦不在C語言層面看,很多東西都顛覆了,所以也不好這樣論,C語言中的定義還按C語言的走吧,他說怎麼算左值怎麼算吧,知道實現過程就行了。

目前為止至少可以說,在這個環境下,以我通過a、b發現的規律來推測,C選項的“參考答案”是錯誤的

C選項應該是a*2 == 8以後再自加1,應該是9;

而D選項,如果可以的話,可能是:a先自加1變5,5*2 == 10以後,10再自加1變11。

但這都是推測,不執行就不敢確認,況且人家的c和d的自增可以在“=”左邊,我使用的a和b都是在“=”右邊的情況,說不定會有特例。。。

以我的這個環境還真的沒法測出來!遺憾,暫時不能完美解決這個問題。

但畢竟很多人都提到++a當左值的情況了,也許以前gcc有這樣的。

還有很多要注意的事,比如,C和C++ 是不一樣的,C在不同的系統和不同的編譯器下,結果也不同:

為什麼C++中++++a可以而a++++不可以?
其實這取決於++左結合操作符號的操作函式,編譯器中對於++a的呼叫相當於
int operator++ (int)
而++右操作符號操作函式時,相當於這樣,返回的依然是一個int型,所以無論++在a的左邊多少個都是可以的。
const int operator++()
注意這裡返回的是一個const的,const只能作為右值,而不能作為左值的。
所以a++是可以的,但是a++++就不行,因為a++返回的是一個const的int值,而該值是不能改變的,所以a++++不行。

常見小例子分析:

#include<stdio.h>
main(){

        int a = 1;

//      a = a +++++ a;//估計和下邊帶括號的執行順序一樣。
        a = a++ + ++a;
//      a = a + (++(++a));//前邊也提到我的gcc是不允許++a當左值的,所以這種也不用試了
        printf("%d\n",a);
}
~    

//如果寫成a = a +++++ a;會編譯出錯。

[email protected]:/usr/local/C-language# gcc apppppa.c

apppppa.c: In function ‘main’:apppppa.c:5:10: error: lvalue required as increment operand

修改後。[email protected]:/usr/local/C-language# gcc apppppa.c

[email protected]:/usr/local/C-language# ./a.out

5

很特別的一點就是,”a+++++a“中,並不是編譯器簡單的算順序結合,此處空格很重要,能改變性質。

結果呢,沒什麼好說的,先+1變成2,然後2+2變成4,最後+1變成5,下面是過程。
0x80483f5 <main+17>: addl $0x1,0x1c(%esp)

0x80483fa <main+22>: mov 0x1c(%esp),%eax

0x80483fe <main+26>: add %eax,%eax

0x8048400 <main+28>: mov %eax,0x1c(%esp)

0x8048404 <main+32>: addl $0x1,0x1c(%esp)

有人的機子號稱跑出了4的結果,還是GCC,可惜沒說什麼版本,多少位。即使不知道自己的GCC什麼版本,不知道自己系統的彙編怎麼一個過程,他也能解釋得跟結果一樣:

a = a++ + ++a;

他的解釋是a++的結果是1。然後++a時a初始是2,++後變成3。結果就是a=1 + 3也就是4

也就是說在第三個加號之前,a++在表示式中就已經生效了,那還要++a幹嘛(真有這種版本的GCC?)所以這種事,有點馬後炮的感覺,你根據你機子的結果,猜測這個結合過程和順序,這完全沒有任何意義,沒有環境和結果讓你說,那就沒結論了。

畢竟人家執行也出現了結果4,也不敢一棒子打死,保留意見吧。也許,他把表示式寫在printf裡了——那4就很好解釋了。。。


既然都不允許當左值了,那麼想當然:

++++a;

a++++;這種在我這都不可能允許。

PS:

如何答這道題

記得幾點就好了,首先知道左值右值這種基本概念,然後,可以”考慮“(只是考慮,靠譜不靠譜需要進一步詳查資料)說下一般認為賦值表示式右邊先執行。

然後,拿出撒手鐗,告訴他“和編譯器有關,至少我的xxxx編譯器是那樣的~!”,

然後可以試著“分析”:“我查看了Linux(AT&T)巨集彙編,是把前自增放在整個式子前邊,後自增放在整個表示式後邊,把整個賦值語句當做一個整體,不分左右”

如果有需要,可以進一步查C語言相關資料,這還包括不同版本的區別,比如C99、ANSI C、C89、K&R C

gcc下的語言規範設定:
-std=iso9899:1990,-ansi或-std=c89 (三者完全等同)來指定完全按照c89規範,而禁止gcc對c語言的擴充套件。
-std=iso9899:199409 使用C95規範
-std=c99 或者 -std=iso9899:1999 使用C99規範。
-std=gnu89 使用c89規範加上gcc自己的擴充套件(目前預設)
-std=gnu99 使用c99規範加上gcc自己的擴充套件

不知道這能否證明我這個結論和語言規範無關:


=========================================================================================================================

2016.02.21補充:

討論左右的自增順序是說的賦值表示式"="兩邊,而不是“=”右側,這算同一邊,在同一邊的話,前自增都是前自增,後自增都是後自增。(又因為很多編譯器,比如gcc,不允許賦值操作符左側有自增操作,認為這不是個左值,所以左邊自增的問題也就不用討論了,問題被簡化了

a = (++a) + (++a);//不會出現因為右邊++a先執行而確定為x,左邊的++a後執行和確定為x+1導致結果是2x+1的情況,結果其實是2x+2。

再不行改成後自增操作:

a=(a++)+(a++);//表示式的結果是2x,但是那是表示式的結果,如果問你最後a是多少,那個後自增得算上,是2x+1。

總之,表示式本身都是偶數的。。。。。。

a = x;

b = a+++a;//b的值是表示式的值,就是(a++)+a;等於2x

a = a+++a;//a的值,當時也是2x,但是過後有個後自增呢,所以過後再取的話a的值是2x+1

a+++a;//因為有一個a的後自增在裡邊,所以最後a的值變成x+1。至於問表示式的值,就和上邊b一樣了。

三者的區別要認清。或者也看是問你表示式結果還是a的值或者b的值。

就看問題問的是什麼東西,表示式,還是詞句結束後某變數的值。

因為在同一表示式內自增自減操作無關於順序

(a++)+a;與a+(a++);等價

++a+a;與a+(++a);等價


另外,關於一長串自增符號的預設結合律,目前的gcc看的話都是左結合的。

a+++a;//等價於(a++)+a;

a+++++a;//等價於((a++)++)+a;

回顧了一下前文a++ + ++a;//經過實際操作與和網友的交流,空格在語法上能起到括號的作用?聽說是的!至少在運算子結合優先順序上,是有所改變的。

這樣說也不對,3*5+6是21,3* 5+6還是21,只有3*(5+6)才是33.
所以這個空格也就在自增運算子那才起到類似括號的作用,空格頂替括號不是常態。 

再參照一下優先順序表,一般是單目運算子優於雙目運算子,而自增同樣也是優於加減法的,這個沒疑問,就是“+”太多的時候,“+”到底被看成自增還是看成加法比較頭疼。

- 負號運算子 -表示式 右到左 單目運算子
(型別) 強制型別轉換 (資料型別)表示式
++ 自增運算子 ++變數名/變數名++ 單目運算子
-- 自減運算子 --變數名/變數名-- 單目運算子
* 取值運算子 *指標表示式 單目運算子
& 取地址運算子 &左值表示式 單目運算子
! 邏輯非運算子 !表示式 單目運算子
~ 按位取反運算子 ~表示式 單目運算子
sizeof 長度運算子 sizeof 表示式/sizeof(型別)
3 / 表示式/表示式 左到右 雙目運算子
* 表示式*表示式 雙目運算子
% 餘數(取模) 整型表示式%整型表示式 雙目運算子
4 + 表示式+表示式 左到右 雙目運算子
- 表示式-表示式 雙目運算子
5 << 左移 表示式<<表示式 左到右 雙目運算子
>> 右移 表示式>>表示式 雙目運算子
後邊的綜合性不知道怎麼解釋的
結合性
( ) [ ] -> . ++(字尾自增) --(字尾自減) left to right
! ~ ++(字首自增) --(字首自減) + - * sizeof(type) right to left
這個優先順序怎麼理解,字尾是左往右,字首是右往左?應該不是這個意思,因為例項是右往左根本沒有機會,別說三個、哪怕五個“+”都不能搶兩個過來,
- 負號運算子 -表示式 右到左 單目運算子
(型別) 強制型別轉換 (資料型別)表示式
++ 自增運算子 ++變數名/變數名++ 單目運算子
-- 自減運算子 --變數名/變數名-- 單目運算子
* 取值運算子 *指標表示式 單目運算子
& 取地址運算子 &左值表示式 單目運算子
! 邏輯非運算子 !表示式 單目運算子
~ 按位取反運算子 ~表示式 單目運算子
sizeof 長度運算子 sizeof 表示式/sizeof(型別)
3 / 表示式/表示式 左到右 雙目運算子
* 表示式*表示式 雙目運算子
% 餘數(取模) 整型表示式%整型表示式 雙目運算子
4 + 表示式+表示式 左到右 雙目運算子
- 表示式-表示式 雙目運算子
5 << 左移 表示式<<表示式 左到右 雙目運算子
>> 右移 表示式>>表示式 雙目運算子

相關推薦

linux selectpoll實現機制例項分析

    我們直到上層對檔案操作結合select與poll可以實現阻塞操作,那麼究竟是如何實現的呢? select介面:     int select(int nfds, fd_set *readset, fd_set *writeset,                fd

C語言++aa++的實現機制操作符結合優先順序

看到一道“經典Linux C“面試題,關於左值和右值的。 華為筆試題 1.寫出判斷ABCD四個表示式的是否正確, 若正確, 寫出經過表示式中 a的值(3分) int a = 4; (A)a += (a++); (B) a += (++a) ;(C) (a++) += a;

關於Javac++隱藏、重寫不同實現機制的探討

tail namespace 文獻 ide archive pretty proc font 分開 一、文章來由 本人如今用c++很多其它。可是曾經Java也寫過不少,Java和c++非常像,可是深入挖一些,Java跟c++的差別非常大,就拿剛剛發的另

C語言可變長引數列表原理實現

可變引數在程式設計中的實現。 stdarg.h標準庫提供的巨集支援了可變長引數列表的使用。 當然,在一些情況下也可以自己通過其實現原理來使用可變長引數程式設計。 條件一: C語言程式設計中函式的形參入棧順序都是從右至左。棧的生長方向是,低地址《—— 高地

C語言編程 遞歸方法非遞歸方法 實現將參數字符串中的字符反向排列

%s png images while char s proc 意義 strlen process //題目要求要求:不能使用C函數庫中的字符串操作函數(否則本題也沒什麽意義了啊) <1>非遞歸方法此方法基本思想是設立兩個指針,分別指向字符串的頭尾並且依次交換所

[轉]c語言宏定義#define的理解資料整理

執行 跟蹤 single 字母 number 而是 字符串 endif 一段 原文地址:http://www.cnblogs.com/haore147/p/3646934.html 1. 利用define來定義 數值宏常量   #define 宏定義是個演技非常高超的替

C語言:二維數組指針實踐1

mvc 數組 vpx c99 mar ebe inf xsl ndt 實1r遜5駛誹喜濟51http://docstore.docin.com/psb360 毓V私陶塹4v31Fhttp://www.docin.com/zucga0192 53o97gw蓖沙賭2yh

C#用ComboBox控件實現市的聯動效果的方法

cat 數據 就是 mode var aio 默認 tchar bottom 本文實例講述了C#用ComboBox控件實現省與市的聯動效果的方法。分享給大家供大家參考。具體實現方法如下: 代碼如下: using System; using System.Collec

C語言中關鍵詞static的用法作用域

細心 錯誤 不同 color 運行程序 可能 gpo 需要 之間 一、面向過程設計中的static 轉載:http://blog.csdn.net/celerylxq/article/details/6160499 1、靜態全局變量 在全局變量前,加上關鍵字stati

C++語言學習(四)——類對象

clas 進行 自身 ngs 符號表 方法 index clu 每一個 C++語言學習(四)——類與對象 一、構造函數(constructor) 1、構造函數簡介 C++語言中,構造函數是與類名相同的特殊成員函數。在類對象創建時,自動調用構造函數,完成類對象的初始化。類對象

C++語言學習(十)——繼承派生

child mem 公有 char 單繼承 同名成員函數 重定義 重載函數 顯示 C++語言學習(十)——繼承與派生 一、類之間的關系 1、類之間的組合關系 組合關系是整體與部分的關系。組合關系的特點:A、將其它類的對象作為當前類的成員使用B、當前類的對象與成員對象的生命周

初識C語言之基本編程思想基本概念掃盲

預編譯 mingw 1.5 集成開發環境 運算 集成 思想 多任務 運行程序 h3 { margin-top: 0.46cm; margin-bottom: 0.46cm; direction: ltr; line-height: 173%; text-align: jus

c語言裡面變數初始化問題Java區別

C語言中,定義區域性變數時如果未初始化,則值是隨機的,為什麼? 定義區域性變數,其實就是在棧中通過移動棧指標來給程式提供一個記憶體空間和這個區域性變數名繫結。因為這段記憶體空間在棧上,而棧記憶體是反覆使用的(髒的,上次用完沒清零的),所以說使用棧來實現的區域性變數定義時如果不顯式初始化,值

C語言中儲存類別、連結記憶體管理

  第12章 儲存類別、連結和記憶體管理 通過記憶體管理系統指定變數的作用域和生命週期,實現對程式的控制。合理使用記憶體是程式設計的一個要點。 12.1 儲存類別 C提供了多種不同的模型和儲存類別,在記憶體中儲存資料。 被儲存的每一個值都佔用一定的實體記憶體;C語言把這樣一塊記憶體稱為物件

c語言陣列中a和&a[0]的區別

p=a與p=&a[o] 等價解釋: p=&a[0] 與 p=a 等價是指,a和&a[0] 指向同一個地址(只是表示的意義不一樣)。 a是整個元素的地址,也就是陣列的起始地址,而&a[0]是陣列首元素a[0]的地址,所以他們指向的地址是相同的. 這兩者的

C語言 inline行內函數帶參巨集

C語言 inline行內函數與帶參巨集 一、簡述         簡單的介紹inline行內函數、帶參巨集的作用。 二、函式的執行與呼叫         函式執行:會將之前的棧的頂,棧基址壓棧,並在棧中開

C語言面向物件程式設計:封裝繼承(1)

最近在用 C 做專案,之前用慣了 C++ ,轉回頭來用C 還真有點不適應。 C++ 語言中自帶面向物件支援,如封裝、繼承、多型等面向物件的基本特徵。 C 原本是面向過程的語言,自身沒有內建這些特性,但我們還是可以利用 C 語言本身已有的特性來實現面向物件的一些基本特徵。接下來我們就一一來細說封裝、繼

c語言中記憶體的動態分配釋放(多維動態陣列構建)

一. 靜態陣列與動態陣列    靜態陣列比較常見,陣列長度預先定義好,在整個程式中,一旦給定大小後就無法再改變長度,靜態陣列自己自動負責釋放佔用的記憶體。    動態陣列長度可以隨程式的需要而重新指定大小。動態陣列由記憶體分配函式(malloc)從堆(heap

zookeeper架構設計基本實現機制

Zookeeper作為一個分散式協調系統提供了一項基本服務:分散式鎖服務,分散式鎖是分散式協調技術實現的核心內容。像配置管理、任務分發、組服務、分散式訊息佇列、分散式通知/協調等,這些應用實際上都是基於這項基礎服務由使用者自己摸索出來的。 1.Zookeeper在大資料系統中的常見應用 zookeeper

C語言查缺補漏(十)#ifndef#endif

忽略點十:#ifndef與#endif ​ 印象中兩者在C/C++專案建立標頭檔案時自動新增,一直沒有深究它的意義,決定跟大家講一下它的用法,順便也是對自己的查缺補漏 ​ 要將他們,首先要說一下專案,對於C語言專案來說,多檔案中的每個檔案的特殊全域性變數,型別定