你所不知道的C語言:指標篇 - HackMD

文章推薦指數: 80 %
投票人數:10人

The construction of a pointer type from a referenced type is called ''pointer type derivation''. 注意到術語!這是C 語言只有call-by-value 的實證,函式的傳遞都 ...        ownedthisnote   Published LinkedwithGitHub Like33 Bookmark Subscribe Edit --- tags:DYKC,C,CLANG,CLANGUAGE,pointer --- #[你所不知道的C語言](https://hackmd.io/@sysprog/c-prog/):指標篇 *「指標」扮演「記憶體」和「物件」之間的橋樑* Copyright(**慣C**)2015,2016,2018[宅色夫](http://wiki.csie.ncku.edu.tw/User/jserv) ==[直播錄影(上)](https://youtu.be/G7vERppua9o)== ==[直播錄影(下)](https://www.youtube.com/watch?v=Owxols1RTAg&feature=youtu.be)== --- ##即便你對指標毫無掌握,你還是能寫程式 ÓlafurWaage在[CppCon2018](https://cppcon.org/cppcon-2018-program/)有個5分鐘的演講"[Let'slearnprogrammingbyinventingit](https://www.youtube.com/watch?v=l5Mp_DEn4bs)"提及,學習C語言的過程,以K&R一書為例,許多人聞風喪膽的「指標」一直到第5章,約全書一半才提到,可解讀為「你可以在不懂指標是什麼之前,仍可掌握多數的C語言功能」。

##這個講座並非「頭腦體操」 *stackoverflow上的[頭腦體操](http://stackoverflow.com/questions/8208021/how-to-increment-a-pointer-address-and-pointers-value/8208106#8208106) 取自[CTrapsandPitfalls](http://www.literateprogramming.com/ctraps.pdf)的案例"UnderstandingDeclarations": ```cpp (*(void(*)())0)(); ``` 可改寫為以下敘述: ```cpp typedefvoid(*funcptr)(); (*(funcptr)0)(); ``` -[][godbolt](http://gcc.godbolt.org/):直接在網頁上看到gcc生成的程式碼 ```cpp intmain(){ typedefvoid(*funcptr)(); (*(funcptr)(void*)0)(); } ``` 對應的組合語言,搭配`-Os`(空間最佳化) ```assembly main: pushq%rax xorl%eax,%eax call*%rax xorl%eax,%eax popq%rdx ret ``` >[source](https://media.giphy.com/media/G10pb1bOz98oE/giphy.gif) 科技公司面試題: ```cpp void**(*d)(int&,char**(*)(char*,char**)); ``` 上述宣告的解讀: *disapointertoafunctionthattakestwoparameters: -areferencetoanintand -apointertoafunctionthattakestwoparameters: -apointertoacharand -apointertoapointertoachar -andreturnsapointertoapointertoachar *andreturnsapointertoapointertovoid [signal系統呼叫](http://man7.org/linux/man-pages/man2/signal.2.html)的宣告方式也很經典: -[Howtoreadthisprototype?](http://stackoverflow.com/questions/15739500/how-to-read-this-prototype) ##Go語言也有指標 1999年4月27日,KenThompson和DennisRitchie自美國總統柯林頓手中接過1998年[NationalMedalofTechnology](https://en.wikipedia.org/wiki/National_Medal_of_Technology_and_Innovation)(國家科技獎章),隔年12月,時年58歲的KenThompson自貝爾實驗室退休。

KenThompson自貝爾實驗室退休後成為一名飛行員。

大概是整日翱翔天際,獲得頗多啟發,在2006年,他進入Google工作,隔年他和過去貝爾實驗室的同僚[RobPike](https://en.wikipedia.org/wiki/Rob_Pike)及RobertGriesemer等人在公司內部提出嶄新的Go程式語言,後者可用於雲端運算在內的眾多領域。

指標這個好東西,當然也要從C語言帶過去給Go語言,連同美妙的struct。

-根據第一份[GolangTalk](https://talks.golang.org/2009/go_talk-20091030.pdf),RobertGriesemer,KenThompson及RobPike等三人認為,世界在變,但系統語言卻已十年未有劇烈變革 -Go之前的程式語言未能達到: -新增函式庫不是一個正確的方向 -需要重新思考整個架構來開發新的程式語言 在實做層面,pointer和struct往往是成雙成對存在(下方會解釋) ##先羅列你已經知道的部分 {%youtubet5NszbIerYc%} >[DavidBrailsford](http://www.cs.nott.ac.uk/~psadb1/)教授解說C語言指標 *[C語言:超好懂的指標](https://kopu.chat/2017/05/15/c%E8%AA%9E%E8%A8%80-%E8%B6%85%E5%A5%BD%E6%87%82%E7%9A%84%E6%8C%87%E6%A8%99%EF%BC%8C%E5%88%9D%E5%AD%B8%E8%80%85%E8%AB%8B%E9%80%B2%EF%BD%9E/) *[EverythingyouneedtoknowaboutpointersinC](https://boredzo.org/pointers/) *疑惑 *該如何解釋[qsort(3)](https://linux.die.net/man/3/qsort)的參數和設計考量呢? *為何我看到的程式碼往往寫成類似下面這樣? ```cpp structlist**lpp; for(lpp=&list;*lpp!=NULL;lpp=&(*lpp)->next) ``` ##回頭看C語言規格書 在[開發工具和規格標準](https://hackmd.io/s/HJFyt37Mx)篇提過參考第一手資料的重要性,以下ISO/IEC9899(簡稱"C99")和指標相關的描述: -[規格書](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf)(PDF)搜尋"***object***",共出現735處 -搜尋"***pointer***",共出現637處。

有趣的是,許多教材往往不談object,而是急著談論pointer,殊不知,這兩者其實就是一體兩面 -object!=object-oriented -前者的重點在於「資料表達法」,後者的重點在於"everythingisobject" -C11([ISO/IEC9899:201x](http://www.open-std.org/jtc1/sc22/WG14/www/docs/n1570.pdf))/[網頁版](http://port70.net/~nsz/c/c11/n1570.html) -`&`不要都念成and,涉及指標操作的時候,要讀為"addressof" -C99標準[6.5.3.2]Addressandindirectionoperators提到'==&=='address-ofoperator -C99[3.14]***object*** -regionofdatastorageintheexecutionenvironment,thecontentsofwhichcanrepresentvalues -在C語言的物件就指在執行時期,==資料==儲存的區域,可以明確表示數值的內容 -很多人誤認在C語言程式中,(int)7和(float)7.0是等價的,其實以資料表示的角度來看,這兩者截然不同,前者對應到二進位的"111",而後者以IEEE754表示則大異於"111" -C99[6.2.4]***Storagedurationsofobjects*** -Anobjecthasastoragedurationthatdeterminesitslifetime.Therearethreestoragedurations:static,automatic,andallocated. >注意生命週期(lifetime)的概念,中文講「初始化」時,感覺像是「盤古開天」,很容易令人誤解。

其實initialize的[英文意義](http://dictionary.reference.com/browse/initialize)很狹隘:"toset(variables,counters,switches,etc.)totheirstartingvaluesatthebeginningofaprogramorsubprogram." -Thelifetimeofanobjectistheportionofprogramexecutionduringwhichstorageisguaranteedtobereservedforit.Anobjectexists,hasaconstantaddressandretainsitslast-storedvaluethroughoutitslifetime.Ifanobjectisreferredtooutsideofitslifetime,thebehaviorisundefined. >在object的生命週期以內,其存在就意味著有對應的常數記憶體位址。

注意,C語言永遠只有call-by-value -Thevalueofapointerbecomesindeterminatewhentheobjectitpointstoreachestheendofitslifetime. >作為object操作的「代名詞」(alias)的pointer,倘若要在object生命週期以外的時機,去取出pointer所指向的object內含值,是未知的。

考慮先做`ptr=malloc(size);free(ptr);`倘若之後做`*ptr`,這個allocatedstorage已經超出原本的生命週期 -Anobjectwhoseidentifierisdeclaredwithnolinkageandwithoutthestorage-classspecifierstatichasautomaticstorageduration. -C99[6.2.5]***Types*** -Apointertypemaybederivedfromafunctiontype,anobjecttype,oranincompletetype,calledthereferencedtype.Apointertypedescribesanobjectwhosevalueprovidesareferencetoanentityofthereferencedtype.ApointertypederivedfromthereferencedtypeTissometimescalled‘‘pointertoT’’.Theconstructionofapointertypefromareferencedtypeiscalled‘‘pointertypederivation’’. >注意到術語!這是C語言只有call-by-value的實證,函式的傳遞都涉及到數值 >這裡的"incompletetype"要注意看,稍後會解釋。

要區分`char[]`和`char*` -Arithmetictypesandpointertypesarecollectivelycalledscalartypes.Arrayandstructuretypesarecollectivelycalledaggregatetypes. >注意"scalartype"這個術語,日後我們看到`++`,`--`,`+=`,`-=`等操作,都是對scalar(純量) >[[來源](http://www.cyut.edu.tw/~cpyu/oldphweb/chapter3/page3.htm)]純量只有大小,它們可用數目及單位來表示(例如溫度=30^o^C)。

純量遵守算數和普通的代數法則。

注意:純量有「單位」(可用`sizeof`操作子得知單位的「大小」),假設`ptr`是個pointertype,對`ptr++`來說,並不是單純`ptr=ptr+1`,而是遞增或遞移1個「單位」 -Anarraytypeofunknownsizeisanincompletetype.Itiscompleted,foranidentifierofthattype,byspecifyingthesizeinalaterdeclaration(withinternalorexternallinkage).Astructureoruniontypeofunknowncontentisanincompletetype.Itiscompleted,foralldeclarationsofthattype,bydeclaringthesamestructureoruniontagwithitsdefiningcontentlaterinthesamescope. >這是C/C++常見的forwarddeclaration技巧的原理,比方說我們可以在標頭檔宣告`structGraphicsObject;`(不用給細部定義)然後`structGraphicsObject*initGraphics(intwidth,intheight);`是合法的,但`structGraphicsObjectobj;`不合法 -Array,function,andpointertypesarecollectivelycalledderiveddeclaratortypes.AdeclaratortypederivationfromatypeTistheconstructionofaderiveddeclaratortypefromTbytheapplicationofanarray-type,afunction-type,orapointer-typederivationtoT. >這句話很重要,貌似三個不相關的術語「陣列」、「函式」,及「指標」都歸類為deriveddeclaratortypes,讀到此處會感到驚訝者,表示不夠理解C語言 :::info "derivative"這詞在是微積分學中就是導數。

一個函數在某一點的導數描述了這個函數在這一點附近的變化率。

導數的本質是通過極限的概念對函數進行局部的線性逼近。

(一個實值函數的圖像曲線。

函數在一點的導數等於它的圖像上這一點處之切線的斜率) :notes:derivative的KK音標是[dəˋrɪvətɪv],而derivation的KK音標是[d,ɛrəv'eʃən] ::: 回到C語言,你看到一個數值,是scalar,但可能也是自某個型態衍生出的declaratortypederivation,實際對應到array,function,pointer等型態的derivation **(練習題)**設定絕對地址為`0x67a9`的32-bit整數變數的值為`0xaa6`,該如何寫? ```cpp *(int32_t*const)(0x67a9)=0xaa6; /*Lvalue*/ ``` -Apointertovoidshallhavethesamerepresentationandalignmentrequirementsasapointertoacharactertype. >關鍵描述!規範`void*`和`char*`彼此可互換的表示法 ```cpp void*memcpy(void*dest,constvoid*src,size_tn); ``` ##英文很重要 安裝`cdecl`程式,可以幫你產生C程式的宣告。

```shell $sudoapt-getinstallcdecl ``` 使用案例: ```shell $cdecl cdecl>declareaasarrayofpointertofunctionreturningpointertofunctionreturningpointertochar ``` 會得到以下輸出: ```cpp char*(*(*a[])())() ``` 把前述C99規格的描述帶入,可得: ```shell cdecl>declarearrayofpointertofunctionreturningstructtag ``` ```cpp structtag(*var[])() ``` 如果你沒辦法用英文來解說C程式的宣告,通常表示你不理解! `cdecl`可解釋C程式宣告的意義,比方說: ```shell cdecl>explainchar*(*fptab[])(int) declarefptabasarrayofpointertofunction(int)returningpointertochar ``` ##`void*`之謎 -`void`在最早的C語言是不存在的,直到C89才確立,為何要設計這樣的型態呢? -[最早的C語言中](https://www.bell-labs.com/usr/dmr/www/primevalC.html),任何函式若沒有特別標注返回型態,一律變成`int`(伴隨著`0`作為返回值),但這導致無從驗證functionprototype和實際使用的狀況 -`void*`的設計,導致開發者必須透過==explicit(顯式)==或強制轉型,才能存取最終的object,否則就會丟出編譯器的錯誤訊息,從而避免危險的指標操作 -我們無法直接對`void*`做數值操作 ```cpp void*p=...; void*p2=p+1;/*whatexactlyisthesizeofvoid?*/ ``` -換句話說,`void*`存在的目的就是為了強迫使用者使用==顯式轉型==或是==強制轉型==,以避免Undefinedbehavior產生 -C/C++[Implicitconversion](http://en.cppreference.com/w/cpp/language/implicit_conversion)vs.[Explicittypeconversion](https://en.cppreference.com/w/cpp/language/explicit_cast) -C99對signextension的[定義和解說](https://www.ptt.cc/bbs/C_and_CPP/M.1460791524.A.603.html) -對某硬體架構,像是ARM,我們需要額外的==alignment==。

ARMv5(含)以前,若要操作32-bit整數(uint32_t),該指標必須對齊32-bit邊界(否則會在dereference時觸發exception)。

於是,當要從`void*`位址讀取uint16_t時,需要這麼做: ```cpp /*mayreceivewrongvalueifptrisnot2-bytealigned*/ /*portablewayofreadingalittle-endianvalue*/ uint16_tvalue=*(uint16_t*)ptr; uint16_tvalue=*(uint8_t*)ptr|((*(uint8_t*)(ptr+1))<<8); ``` 延伸閱讀:[記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory) ###`void*`真的萬能嗎? 依據C99規格6.3.2.3:8[Pointers] >Apointertoafunctionofonetypemaybeconvertedtoapointertoafunctionofanothertypeandbackagain;theresultshallcompareequaltotheoriginalpointer.Ifaconvertedpointerisusedtocallafunctionwhosetypeisnotcompatiblewiththepointed-totype,thebehaviorisundefined. 換言之,C99不保證pointerstodata(inthestandard,"objectsorincompletetypes"e.g.`char*`or`void*`)和pointerstofunctions之間相互轉換是正確的 -可能會招致undefinedbehavior(UB) -注意:C99規範中,存在一系列的UB,詳見[未定義行為篇](https://hackmd.io/@sysprog/c-undefined-behavior) >延伸閱讀:[TheLostArtofStructurePacking](http://www.catb.org/esr/structure-packing/) ##沒有「雙指標」只有「指標的指標」 「雙馬尾」(左右「獨立」的個體)和「馬尾的馬尾」(由單一個體關聯到另一個體的對應)不同 -中文的「[雙](https://www.moedict.tw/%E9%9B%99)」有「對稱」且「獨立」的意含,但這跟「指標的指標」行為完全迥異 -講「==雙==指標」已非「懂不懂C語言」,而是漢語認知的問題 C語言中,萬物皆是數值(everythingisavalue),函式呼叫當然只有call-by-value。

「指標的指標」(英文就是apointerofapointer)是個常見用來改變「傳入變數原始數值」的技巧。

考慮以下程式碼: ```cpp=1 intB=2; voidfunc(int*p){p=&B;} intmain(){ intA=1,C=3; int*ptrA=&A; func(ptrA); printf("%d\n",*ptrA); return0; } ``` 在第5行(含)之前的記憶體示意: ```graphviz digraphstructs{ node[shape=record] {rank=same;structa,structb,structc} structptr[label="ptrA|&A"]; structb[label="B|2"]; structa[label="A|1"]; structc[label="C|3"]; structptr:ptr->structa:A:nw } ``` 第6行`ptrA`數值傳入`func`後的記憶體示意: ```graphviz digraphstructs{ node[shape=record] {rank=same;structa,structb,structc} structp[label="p|

&A"] structptr[label="ptrA|&A"]; structb[label="B|2"]; structa[label="A|1"]; structc[label="C|3"]; structptr:ptr->structa:A:nw structp:p->structa:A:nw } ``` -`func`依據[函式呼叫篇](https://hackmd.io/s/HJpiYaZfl)描述,將變數`p`的值指定為`&B` ```graphviz digraphstructs{ node[shape=record] {rank=same;structa,structb,structc} structp[label="p|

&B"] structptr[label="ptrA|&A"]; structb[label="B|2"]; structa[label="A|1"]; structc[label="C|3"]; structptr:ptr->structa:A:nw structp:p->structb:B:nw } ``` 由上圖可見,原本在main中的`ptrA`內含值沒有改變。

這不是我們期望的結果,該如何克服呢?可透過「指標的指標」來改寫,如下: ```cpp=1 intB=2; voidfunc(int**p){*p=&B;} intmain(){ intA=1,C=3; int*ptrA=&A; func(&ptrA); printf("%d\n",*ptrA); return0; } ``` 在第5行(含)之前的記憶體示意: ```graphviz digraphstructs{ node[shape=record] {rank=same;structa,structb,structc} structptr[label="ptrA|&A"]; structb[label="B|2"]; structa[label="
A|1"]; structc[label="C|3"]; structptr:ptr->structa:A:nw } ``` 第6行時,傳入`func`的參數是`&ptrA`,也就是下圖中的`&ptrA(temp)`: ```graphviz digraphstructs{ node[shape=record] {rank=same;structa,structb,structc} structadptr[label="&ptrA(temp)|&ptrA"] structptr[label="ptrA|&A"]; structb[label="B|2"]; structa[label="A|1"]; structc[label="C|3"]; structptr:ptr->structa:A:nw structadptr:adptr->structptr:name_ptr:nw } ``` 進入func執行後,在編譯器產生對應參數傳遞的程式碼中,會複製一份剛傳入的`&ptrA`,產生一個自動變數`p`,將`&ptrA`內的值存在其中,示意如下: ```graphviz digraphstructs{ node[shape=record] {rank=same;structa,structb,structc} structp[label="p(infunc)|

&ptrA"] structadptr[label="&ptrA(temp)|&ptrA"] structptr[label="ptrA|&A"]; structb[label="B|2"]; structa[label="A|1"]; structc[label="C|3"]; structptr:ptr->structa:A:nw structadptr:adptr->structptr:name_ptr:nw structp:p->structptr:name_ptr:nw } ``` 在`func`中把`p`指向到的值換成`&B`: ```graphviz digraphstructs{ node[shape=record] {rank=same;structa,structb,structc} structp[label="p(infunc)|

&ptrA"] structadptr[label="&ptrA(temp)|&ptrA"] structptr[label="ptrA|&B"]; structb[label="B|2"]; structa[label="A|1"]; structc[label="C|3"]; structptr:ptr->structb:B:nw structadptr:adptr->structptr:name_ptr:nw structp:p->structptr:name_ptr:nw } ``` 經過上述「指標的指標」,`*ptrA`的數值從1變成2,而且`ptrA`指向的物件也改變了。

延伸閱讀: *[CLinkedListImplementationPointervs.Pointer-to-PointerConsistency](https://softwareengineering.stackexchange.com/questions/260411/c-linked-list-implementation-pointer-vs-pointer-to-pointer-consistency) *[你所不知道的C語言:linkedlist和非連續記憶體操作](https://hackmd.io/s/SkE33UTHf) ##forwarddeclaration搭配指標的技巧 案例:[oltk](https://github.com/openmoko/system-test-suite/tree/master/gta02-dm2/src/oltk)是[Openmoko](https://en.wikipedia.org/wiki/Openmoko)為了工廠測試而開發出的精簡繪圖系統,支援觸碰螢幕,程式碼不到1000行C語言程式。

執行畫面:(oltk的開發者是[olv](https://github.com/olvaffe)) ![](https://hackpad-attachments.s3.amazonaws.com/embedded2015.hackpad.com_s0rlzR8wVtm_p.299401_1448120302988_oltk-dm2.png) -[][oltk.h](https://github.com/openmoko/system-test-suite/blob/master/gta02-dm2/src/oltk/oltk.h) ```cpp structoltk;//宣告(incompletetype,void) structoltk_button; typedefvoidoltk_button_cb_click(structoltk_button*button,void*data); typedefvoidoltk_button_cb_draw(structoltk_button*button, structoltk_rectangle*rect,void*data); structoltk_button*oltk_button_add(structoltk*oltk, intx,inty, intwidth,intheight); ``` `structoltk`和`structoltk_button`沒有具體的定義(definition)或實做(implementation),僅有宣告(declaration)。

-[][oltk.c](https://github.com/openmoko/system-test-suite/blob/master/gta02-dm2/src/oltk/oltk.c) ```cpp structoltk{ structgr*gr; structoltk_button**zbuttons; ... structoltk_rectangleinvalid_rect; }; ``` 軟體界面(interface)揭露於oltk.h,不管structoltk內容怎麼修改,已公開的函式如`oltk_button_add`都是透過pointer存取給定的位址,而不用顧慮具體structoltk的實做,如此一來,不僅可以隱藏實作細節,還能兼顧二進位的相容性(binarycompatibility)。

同理,structoltk_button不管怎麼變更,在structoltk裡面也是用給定的pointer去存取,保留未來實做的彈性。

延伸閱讀 *[你所不知道的C語言:物件導向程式設計篇](https://hackmd.io/@sysprog/c-oop) *[如何在指標中隱藏資料?](https://itw01.com/5OSE3G9.html)/[原文](https://arjunsreedharan.org/post/105266490272/hide-data-inside-pointers) ##Pointersvs.Arrays -arrayvs.pointer -indeclaration -extern,如`externcharx[];`$\to$不能變更為pointer的形式 -definition/statement,如`charx[10]`$\to$不能變更為pointer的形式 -parameteroffunction,如`func(charx[])`$\to$可變更為pointer的形式$\to$`func(char*x)` -inexpression -array與pointer可互換 ```cpp intmain(){ intx[10]={0,1,2,3,4,5,6,7,8,9}; printf("%d%d%d%d\n",x[4],*(x+4),*(4+x),4[x]); } ``` 在[TheCProgrammingLanguage](http://www.amazon.com/The-Programming-Language-Brian-Kernighan/dp/0131103628)第2版,Page99寫道: >Asformalparametersinafunctiondefinition, 該書Page100則寫: >`chars[]`;and`char*s`areequivalent. :::info -這就是悲劇的由來,人們往往忘了前一頁 -`x[i]`總是被編譯器改寫為`*(x+i)`$\leftarrow$inexpression ::: -C提供操作多維陣列的機制(C99[6.5.2.1]**_Arraysubscripting_**),但實際上只有一維陣列的資料存取 -對應到線性記憶體 -Successivesubscriptoperatorsdesignateanelementofamultidimensionalarrayobject.IfEisann-dimensionalarray(n≥2)withdimensionsi×j×...×k,thenE(usedasotherthananlvalue)isconvertedtoapointertoan(n−1)-dimensionalarraywithdimensionsj×...×k.Iftheunary*operatorisappliedtothispointerexplicitly,orimplicitlyasaresultofsubscripting,theresultisthepointed-to(n−1)-dimensionalarray,whichitselfisconvertedintoapointerifusedasotherthananlvalue.Itfollowsfromthisthatarraysarestoredinrow-majororder(lastsubscriptvariesfastest) -Considerthearrayobjectdefinedbythedeclaration`intx[3][5];`Herexisa3×5arrayofints;moreprecisely,xisanarrayofthreeelementobjects,eachofwhichisanarrayoffiveints.Intheexpression`x[i]`,whichisequivalentto`(*((x)+(i)))`,xisfirstconvertedtoapointertotheinitialarrayoffiveints.Theniisadjustedaccordingtothetypeofx,whichconceptuallyentailsmultiplyingibythesizeoftheobjecttowhichthepointerpoints,namelyanarrayoffiveintobjects.Theresultsareaddedandindirectionisappliedtoyieldanarrayoffiveints.Whenusedintheexpression`x[i][j]`,thatarrayisinturnconvertedtoapointertothefirstoftheints,so`x[i][j]`yieldsanint. -arraysubscripting在**編譯時期**只確認以下兩件事: -得知size -取得陣列第0個(即最初的一項)元素的指標 -前兩者以外的操作,都透過pointer -arraysubscripting=>syntaxsugar -Arraydeclaration: ```cpp inta[3]; struct{doublev[3];doublelength;}b[17]; intcalendar[12][31]; ``` 那麼... ```cpp sizeof(calendar)=?sizeof(b)=? ``` 善用GDB,能省下沒必要的`printf()`,並可互動分析:(下方以GNU/Linuxx86_64作為示範平台) -有時會遇到程式邏輯和結果正確,但因為`printf()`的輸出格式沒用對,而誤以為自己程式沒寫好的狀況 ```shell (gdb)psizeof(calendar) $1=1488 (gdb)print1488/12/31 $2=4 (gdb)psizeof(b) $3=544 ``` 還可分析型態: ```shell (gdb)whatiscalendar type=int[12][31] (gdb)whatisb[0] type=struct{...} (gdb)whatis&b[0] type=struct{...}* ``` 更可直接觀察和修改記憶體內容: ```shell (gdb)x/4b 0x601060:0x000000000x000000000x000000000x00000000 (gdb)pb $4={{ v={0,0,0}, length=0 }} ``` 終於可來作實驗: ```shell (gdb)p&b $5=(struct{...}(*)[17])0x601060 (gdb)p&b+1 $6=(struct{...}(*)[17])0x601280
``` 上一行`&b+1`指向的位址,就是`inta[3]`的所在處?!來確認: ```shell (gdb)p&a[0] $7=(int*)0x601280 ``` 那`&b[0]+1`指向哪呢? ```shell (gdb)p&b[0]+1 $8=(struct{...}*)0x601080 (gdb)psizeof(b[0]) $9=32 ``` 原來`&b[0]+1`的`+1`就是遞移一個`b[0]`佔用的空間。

提昇輸出的可讀性 ```shell (gdb)setprintpretty ``` 繼續觀察記憶體內容: ```cpp (gdb)p&b[0] $10=(struct{...}*)0x601060 (gdb)p(&b[0])->v $11={0,0,0} ``` `p`命令不僅能print,還可變更記憶體內容: ```cpp (gdb)p(&b[0])->v={1,2,3} $12={1,2,3} (gdb)pb[0] $13={ v={1,2,3}, length=0 } ``` 還記得前面提到`(float)7.0`和`(int)7`的不同嗎?我們來觀察執行時期的表現: ```cpp (gdb)whatis(&b[0])->v[0] type=double (gdb)psizeof(&b[0])->v[0] $14=8 ``` Linuxx86_64採用[LP64datamodel](https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models),`double`依據C語言規範,至少要有64-bit長度。

現在試著強制轉型: ```cpp (gdb)p&(&b[0])->v[0] $15=(double*)0x601060 (gdb)p(int*)&(&b[0])->v[0] $16=(int*)0x601060 (gdb)p*(int*)&(&b[0])->v[0] $17=0 ``` 竟然變成`0`,為什麼?! 因為: ```shell (gdb)psizeof(int) $18=4 ``` 我們只取出`v[0]`開頭的4bytes,轉型為`int`後,內容就是0。

印出記憶體來觀察: ```cpp (gdb)x/4(int*)&(&b[0])->v[0] 0x601060:0x000000000x3ff000000x000000000x40000000 ``` GDB強大之處不只如此,你甚至在動態時期可呼叫函式(改變執行順序),比方說`memcpy`: ```cpp (gdb)pcalendar $19={{0}} (gdb)pmemcpy(calendar,b,sizeof(b[0])) $20=6296224 (gdb)pcalendar $21={{0,1072693248,0,1073741824,0,1074266112,0},{0}} ``` 現在`calendar[][]`內容已變更。

前述輸出有個數字`6296224`,到底是什麼呢?試著觀察: ```cpp (gdb)p(void*)6296224 $22=(void*)0x6012a0 ``` 原來就是`memcpy`的目的位址,符合[memcpy(3)](http://linux.die.net/man/3/memcpy)的描述。

從calendar把`{1,2,3}`內容取出該怎麼作呢? ```cpp (gdb)p*(double*)&calendar[0][0] $23=1 (gdb)p*(double*)&calendar[0][2] $24=2 (gdb)p*(double*)&calendar[0][4] $25=3 ``` -Pointersmayhappentopointtoanelementofanarray -Wecanusethatpointertoobtainapointertothenext/previouselementofthatarray -Add/Subtract1forthatpointer ```cpp inta[10]; ... int*p; p=a;/*takethepointertoa[0]*/ p++;/*nextelement*/ p--;/*previouselement*/ ``` -Addinganintegertoapointerisdifferentfromaddingthatintegertothebitrepresentationofthatpointer ```cpp int*p; p=p+1;/*thisadvancesp'svalue(pointed-address)bysizeof(int)whichisusuallynot1*/ ``` -Thenameofthearrayisthesameasapointertothearrayineverycontextbutone ```cpp inta[3]; int*p; p=a;/*ppointstoa[0]*/ *a=5;/*setsa[0]to5*/ *(a+1)=5;/*setsa[1]to5*/ ``` Theonlydifferenceisinsizeof: ```cpp sizeof(a)/*returnsthesizeoftheentirearraynotjusta[0]*/ ``` -Supposewewanttoconcatenatestringsandstringtintoasinglestring ```cpp char*r; strcpy(r,s); strcat(r,t); ``` Doesn'tworkcauserdoesn'tpointanywhere. Letsmakeranarray–nowitpointsto100chars ```cpp charr[100]; strcpy(r,s); strcat(r,t); ``` Thisworksaslongasthestringspointedtobysandtaren’ttoobig. -Wewouldliketowritesomethinglike: ```cpp charr[strlen(s)+strlen(t)]; strcpy(r,s);strcat(r,t); ``` HoweverCrequiresustostatethesizeofthearrayasconstant. ```cpp char*r=malloc(strlen(s)+strlen(t)); strcpy(r,s);strcat(r,t); ``` Thisfailsforthreereasons: 1.`malloc()`mightfailtoallocatetherequiredmemory 2.Itisimportanttofreethememoryallocated 3.Wedidnotallocateenoughmemory Thecorrectcodewillbe: ```cpp char*r=malloc(strlen(s)+strlen(t)+1);//usesbrk;changeprogrambreak if(!r)exit(1);/*printsomeerrorandexit*/ strcpy(r,s);strcat(r,t); ``` /*later*/ ```cpp free(r); r=NULL;/*Trytoresetfree’dpointerstoNULL*/ ``` `intmain(intargc,char*argv[],char*envp[])`的奧秘 ```cpp #include intmain(intargc,char(*argv)[0]) { puts(((char**)argv)[0]); return0; } ``` 使用gdb ```cpp (gdb)bmain (gdb)r (gdb)print*((char**)argv) $1=0x7fffffffe7c9"/tmp/x" ``` 這裡符合預期,但接下來: ```cpp (gdb)x/4s(char**)argv 0x7fffffffe558:"\311\347\377\377\377\177" 0x7fffffffe55f:"" 0x7fffffffe560:"" 0x7fffffffe561:"" ``` 無法立即會意,換個方式解析: ```cpp (gdb)x/4s((char**)argv)[0] 0x7fffffffe7c9:"/tmp/x" 0x7fffffffe7d0:"LC_PAPER=zh_TW" 0x7fffffffe7df:"XDG_SESSION_ID=91" 0x7fffffffe7f1:"LC_ADDRESS=zh_TW" ``` 後三項是envp(environmentvariable) Carray大概是C語言中數一數二令人困惑的部分。

根據你使用的地方不同,Array會有不同的意義: *如果是用在expression,array永遠會被轉成一個pointer *用在functionargument以外的declaration中它還是一個array,而且「不能」被改寫成pointer *functionargument中的array會被轉成pointer 若現在這裡有一個全域變數(global) ```cpp chara[10]; ``` 在另一個檔案中,我不能夠用`externchar*a`來操作原本的`a`,因為實際上會對應到不同的指令(instruction)。

但若你的declaration是在functionargument中,那麼: *`voidfunction(chara[])`與`voidfunction(char*consta)`是等價的 *而且,真實的型態會是pointer 因此你不能用`sizeof`取得其佔用的空間!(array是unmodifiablel-value,所以除了被轉成pointer,它還會是一個不能再被設定數值的指標,因此需要加上`const`修飾。

) 最後,用在取值時,array的行為與pointer幾乎雷同,但array會是用兩步取值,而pointer是三步。

(array的位址本身加上offset,共兩步,而使用pointer時,cpu需先載入pointer位址,再用pointer的值當作位址並加上offset取值) 延伸閱讀 *[C語言雜談:指標和陣列(上)](https://www.cnblogs.com/baochuan/archive/2012/03/22/2410821.html) *[C語言雜談:指標和陣列(下)](https://www.cnblogs.com/baochuan/archive/2012/03/26/2414062.html) *[C語言的指標和記憶體洩漏](http://blog.jobbole.com/84548/) -AcommonCpitfallistoconfuseapointerwiththedatatowhichitpoints ![](https://hackpad-attachments.s3.amazonaws.com/embedded2015.hackpad.com_s0rlzR8wVtm_p.299401_1446626702133_copy.png) 依據manpage[strdup(3)](http://linux.die.net/man/3/strdup): >Thestrdup()functionreturnsapointertoanewstringwhichisaduplicateofthestrings.Memoryforthenewstringisobtainedwith[malloc(3)](http://linux.die.net/man/3/malloc),andcanbefreedwith[free(3)](http://linux.die.net/man/3/free). `strdup()`實際上會呼叫`malloc()`,來配置足以放置給定字串的「新空間」。

##FunctionPointer ```cpp intmain(){return(********puts)("Hello");} ``` 為何可運作? 根據C99規格書 -C99[6.3.2.1]Afunctiondesignatorisanexpressionthathasfunctiontype -**Except**whenitistheoperandofthe**sizeof**operatorortheunary**&**operator,a**functiondesignator**withtype‘‘functionreturningtype’’**isconvertedto**anexpressionthathastype‘‘**pointertofunctionreturningtype’**’. -`*`isRightassociativeoperator -C99[6.5.1]Itisanlvalue,afunctiondesignator,oravoidexpressioniftheunparenthesizedexpressionis,respectively,anlvalue,afunctiondesignator,oravoidexpression. -C99[6.5.3.2-4]Theunary*operatordenotesindirection.**Iftheoperandpointstoafunction,theresultisafunctiondesignator** (注意到designator的發音,KK音標為[͵dɛ**zɪg**ˋnetɚ]) [實驗程式碼](https://gist.github.com/JIElite/d429aedbc515973ad46cd353a43d2388) 程式碼`(********puts)`可看作`(*(*(*(*(*(*(*(*puts))))))))`,最裡面的括號`(*puts)`由[6.5.3.2-4]可知它是一個functiondesignator。

再根據[6.3.2.1],它最後會被轉為pointertofunctionreturningtype。

往外延伸一個括號`(*(*puts))`由於最裡面的括號是一個functiondesignator,再多一個*operator它還是一個functiondesignator,最後也會被轉為pointertofunctionreturningtype,依此類推最後`(********puts)`仍會是一個functiondesignator。

根據6.3.2.1 ```cpp void(*fptr)(); voidtest(); fptr=test; ``` test:void(),會被轉成functionpointer:void(*)() fptrisapointertofunctionwithreturningtype, 根據6.5.3.2 `(*fptr)`isafunctiondesignator,afunctiondesignatorwillbeconvertedtopointer. typeof(*fptr):void() 我們可利用gdb去查看這些數值,搭配print命令: ```shell (gdb)printtest $1={void()}0x400526 (gdb)printtest $2={void()}0x400526 (gdb)printfptr $3=(void(*)())0x400526 (gdb)print(*fptr) $4={void()}0x400526 (gdb)print(**fptr) $5={void()}0x400526 ``` test是一個functiondesignator,因為不是搭配`&`,sizeof使用,所以會被轉成為pointertofunction `(*fptr)`是一個functionpointer搭配*(dereference,indirection)operator使用,則它的結果會被轉成為一個functiondesignator 所以`(**fptr)`要拆成兩個部份:`(*(*fptr))`,裡面的`*fptr`是個functiondesignator,但是還是會被轉成一個pointertofunction,我們可註記為`fptr`。

又,外面又有一個*operator,`(*fptr’`是個functiondesignator,最後還是會被轉化為pointertofunction 但`0x400526`這個數值是怎麼來的呢? 我們可用以下命令觀察: ```shell $gcc-ofp-gfp.c $objdump-dfp ``` 參考輸出裡頭的一段: ```shell 0000000000400526: 400526:55push%rbp 400527:4889e5mov%rsp,%rbp 40052a:90nop 40052b:5dpop%rbp 40052c:c3retq ``` 這個數值其實是執行檔反組譯後的函式進入點,但這數值並非存在實體記憶體的實際數值,而是虛擬記憶載入位址。

由於`puts`的functionprototype是`intputs(constchar*s)`,因此每次經過`*`operator運算後得到的結果是仍然是`int`。

所以`*`的數目不會影響結果。

最後return的值是根據s的長度加上`'\n'`。

而這個例子return給`main`的值是6 ##Addressandindirectionoperators 對應到C99/C11規格書[6.5.3.2],`&`所能操作的operand只能是: *functiondesignator-基本上就是functionname *`[]`or`*`的操作結果:跟這兩個作用時,基本上就是相消 -`*`-operand本身 -`[]`-`&`會消失,而`[]`會被轉換成只剩`+`(註:原本`[]`會是`+`搭配`*`) +例如:`&(a[5])==a+5` *一個指向非bit-fieldorregisterstorage-classspecifier的object的lvalue >[bit-field](https://hackmd.io/@sysprog/c-bitfield):一種在struct或union中使用用來節省記憶體空間的物件; >特別的用途:沒有名稱的bit-field可以做為padding 除了遇到`[]`或`*`外,使用`&`的結果基本上都是得到pointertotheobject或是function的address 考慮以下程式碼: ```cpp charstr[123]; ``` 為何`str==&str`呢? *實際上左右兩邊的型態是不一樣的,只是值相同。

*左邊的是pointertochar:`char*` -規格書中表示:除非遇到sizeof或是&之外,arrayoftype(在這就是指str)都會被直接解讀成pointertotype(在這就是pointertochar),而這個type是根據array的第一個元素來決定的 >Exceptwhenitistheoperandofthesizeofoperatorortheunary&operator,orisastringliteralusedtoinitializeanarray,anexpressionthathastype‘‘arrayoftype’’isconvertedtoanexpressionwithtype‘‘pointertotype’’thatpointstotheinitialelementofthearrayobjectandisnotanlvalue.(C996.3.2.1) *右邊的則是pointertoanarray:`char(*)[123]` -上面提到:遇到&時,str不會被解讀為pointertotype,而是做為原本的object,在這就是arrayobject,而addressofarrayobject也就是這個arrayobject的起始位址,當然也就會跟第一個元素的位址相同 -除了用值相同來解釋外,規格書在提到equalityoperators時,也有說到類似情境 >Twopointerscompareequalifandonlyifbotharenullpointers,botharepointerstothesameobject(includingapointertoanobjectandasubobjectatitsbeginning)orfunction(C996.5.9) ##針對指標的修飾(qualifier) *指標本身不可變更(Constantpointertovariable):`const`在`*`之後 >char==*==constpContent; *指標所指向的內容不可變更(Pointertoconstant):`const`在`*`之前 >constchar==*==pContent; >charconst==*==pContent; *兩者都不可變更 >constchar==*==constpContent; ##LearnCTheHardWay -[書本資訊](https://learncodethehardway.org/c/)/[電子書](https://wizardforcel.gitbooks.io/lcthw/content/) -[Exercise36:SaferStrings](https://wizardforcel.gitbooks.io/lcthw/content/ex36.html) -"**WhyCStringsWereAHorribleIdea**" -itisnotpossibletovalidateaCstringwithoutpossiblyloopinginfinitely. -延伸閱讀:["NULL":電腦科學中最嚴重的錯誤,造成10億美元損失](https://linux.cn/article-6503-1.html) -中華民國刑法第227條:「對於未滿十四歲之少男或少女為性交者,處三年以上十年以下有期徒刑...對於十四歲以上未滿十六歲之男女為性交者,處七年以下有期徒刑」 -刑法第227-1條(兩小無猜條款),規定未滿十八歲的人犯227條時,為告訴乃論,這是考量到雙方都是未成年,因為對性事好奇,或者是彼此相愛而進行性行為,故有特規定這個例外可通融,得免除或減輕其刑。

-[[出處](http://yblog.org/archive/index.php/11764)]規定要18歲以上才可看限制級內容的影片、圖畫和書刊,不過滿16歲就可合法性 -看限制級內容還不如實地操作才學得到東西(實作萬歲!) -有沒有發現跟C-stylestring很像?都「經歷過」,才跟你說以前有「法律疑慮」 -延伸閱讀:[DidKen,Dennis,andBrianchoosewrongwithNUL-terminatedtextstrings?](http://queue.acm.org/detail.cfm?id=2010365) -PerformanceCosts:什麼時候VisualBASIC會比C來得快?像是取得字串的長度,前者是常數時間,但後者就算做了[各式strlen()最佳化](https://coldnew.gitbooks.io/2015-embedded-summber/content/exame/detect_macro.html),終究耗時 -CompilerDevelopmentCost:對於`charmsg[256]`這樣的stringliteral,編譯器最佳化時,會嘗試用`memcpy()`取代`strcpy()`,前者通常有平台相依的最佳化,而後者因為不能事先知道長度,無法做更有效率的最佳化。

但這樣的最佳化技巧要讓編譯器自動施行,導致額外的維護成本 -SecurityCosts:一個`gets()`就有機會搞垮系統,對照看[InsecurecodinginC](http://www.slideshare.net/olvemaudal/insecure-coding-in-c-and-c) -[Exercise15:PointersDreadedPointers](https://www.kancloud.cn/wizardforcel/lcthw/169213) -[Exercise16:StructsAndPointersToThem](https://wizardforcel.gitbooks.io/lcthw/content/ex16.html) -[Exercise18:PointersToFunctions](https://wizardforcel.gitbooks.io/lcthw/content/ex18.html) ##重新探討「字串」 -在電腦科學中,字串是由一連串的符號組成(注意:每單位可能不是1byte),每種程式語言對字串的實作方式或多或少會有不同,C語言沒有直接的語言層面支援,一般用"cstring"或稱為"c-stylestring"(C++觀點)稱呼,cstring又稱為是**ASCIIZ**或**null-terminatedstring** -自C99以來,C有2種字串 1.bytestring:使用char作為最小操作單位,每個char至少要8bits 2.widestring:使用[wchar_t](http://pubs.opengroup.org/onlinepubs/007908775/xsh/wchar.h.html)作為最小操作單位,以便支援Unicode或多國語文(意味著變動程度編碼) -大多數歐語的部分字母都有標示讀音的附加符號(俗稱accentmarks,學術上則稱diacritics),例如法語的**ç**、西班牙語的**ñ**、德語的**ü**、芬蘭語的**å**、波蘭語的**ł**、丹麥語的**ø**等等 -英語在歷史上曾經受過多國語言所沉浸和洗禮,本是歸屬於日耳曼語系,後又受過拉丁語、希臘語、法語、斯堪的納維亞語(即普遍的北歐語)等影響,以致今天英語的讀音變得非常不規則,相比其他歐語,英語的字母失去表音的功能,已不能從字母本身猜出正確讀音。

-延伸閱讀:[字母符號](http://languagemystery.blogspot.tw/2013/05/blog-post_30.html) :::info [UTF-8的發明人](https://www.cl.cam.ac.uk/~mgk25/ucs/utf-8-history.txt)也是主導UNIX發展的RobPike與KenThompson [取自Wikipedia](https://en.wikipedia.org/wiki/C_string_handling): >"SupportforUnicodeliteralssuchas`charfoo[512]="φωωβαρ";`(UTF-8)or`wchar_tfoo[512]=L"φωωβαρ";`(UTF-16orUTF-32)isimplementationdefined,andmayrequirethatthesourcecodebeinthesameencoding.Somecompilersoreditorswillrequireenteringallnon-ASCIIcharactersas`\xNN`sequencesforeachbyteofUTF-8,and/or`\uNNNN`foreachwordofUTF-16." ::: *[[出處](http://luse.blogspot.tw/2009/06/blog-post.html)]為了使用上方便,許多程式語言提供**stringliterals**(在C就是C99[6.4.5]),讓開發者得以在程式碼內表示一些字串。

由於C語言是個系統程式語言,所以大家對這些東西會身在何處是有高度興趣的。

而stringliterals到底會身在何處,和編譯器的實作有關 *C語言規格中定義stringliterals會被分配於"staticstorage"當中(C99[6.4.5]),並說明如果程式嘗試修改stringliterals的內容,將會造成未定義行為 -以gcc的ELFtarget來說,將stringliterals分配在read-onlydatasection中(當然包含`\0`結尾) -由於C語言提供了一些syntaxsugar來初始化陣列,這使得`char*p="helloworld"`和`charp[]="helloworld"`寫法相似,但底層的行為卻大相逕庭 -背景知識:[你所不知道的C語言:函式呼叫篇](https://hackmd.io/s/SJ6hRj-zg)關於stack的描述 -以指標的寫法`char*p`來說,意思是`p`將會是指向staticstorage的一個指標。

如此的寫法有潛在問題,因為當開發者嘗試修改stringliterals的內容,將會造成未定義行為,而編譯器並不會對存取p的元素提出警告 -值得注意的是,陣列的寫法依據C99規範,stringliterals是必須放在"staticstorage"中,而`charp[]`的語意則表示要把資料分配在stack內,所以這會造成編譯器(gcc)隱性地(implicitly)產生額外的程式碼,使得C程式在執行時期可把stringliterals從staticstorage複製到stack中。

雖然字串本身並非存放於stack內,但`charp[]`卻是分配在stack內,這也造成`returnp`是未定義行為 *光有字串表示法是遠遠不夠的,我們需要在字串上進行若干操作。

C語言的規格書中定義了標準函式庫,在[7.21]***Stringhandling\***提及,以避免製造重複的輪子。

接下來問題來了,這些字串處理函式的原型大部分是使用`char*`或`void*`的型別來當參數,那這些參數到底能不能接受nullpointer`(void*)0`呢?如果不能,那函式要負責檢查嗎?nullpointer算是一個字串嗎?對一個nullpointer使用這些函式(如`strlen`)會發生什麼事?規格書中有定義這些東西嗎? -答案是,像`strlen`一類的"stringhandlingfunction"++**不可**++接受nullpointer作為參數,因為在絕大部分的狀況中,nullpointer並非合法字串(why?),所以將nullpointer代入`strlen()`也造成未定義行為 -多數函式庫實作(glibc)也不會在內做nullpointer檢查(有些函式庫的實作會使用編譯器的延伸語法來提出警告),這意味著,開發者要肩負檢查nullpointer的一切責任! *回頭閱讀規格書:[7.21.1]提到以下: >Variousmethodsareusedfordeterminingthelengthsofthearrays,butinallcasesa`char*`or`void*`argumentpointstotheinitial(lowestaddressed)characterofthearray.Ifanarrayisaccessedbeyondtheendofanobject,thebehaviorisundefined. -nullpointer顯然在++**絕大部分**++的狀況都不符合這個規定,既然不符合規定,函式庫的實作顯然也不需要浪費心力去做檢查。

更不用說想要在一些物件導向的字串函式庫中使用string物件的nullpointer來做字串運算。

延伸閱讀 -[SDS:一個簡易動態字串處理機制](http://blog.jobbole.com/68119/) ##LinusTorvalds親自教你C語言 -[arrayargument的正確使用時機](https://hackmd.io/@sysprog/c-array-argument) [source](https://lkml.org/lkml/2015/9/3/541)Arethereanycasesofmulti-dimensionalarrays?Becausethoseactuallyhavesemanticmeaningoutsideof`sizeof()`,justinthingslikeaddingoffsets.Egsomethinglike ```cpp intfn(inta[][10]) ``` -endsupbeingequivalenttosomethinglike ```cpp intfn(int(*a)[10]) ``` and`a+1`isactually40bytesaheadof`a`,soitdoes*not*actlike an"int*". (AndImighthavescrewedthatupmightily-Cmultidimensionalarrays andtheconversionstopointersarereallyeasytogetconfusedabout. WhichiswhyIhopewedon'thavethem) -[藝術與核心](http://blog.linux.org.tw/~jserv/archives/001888.html) ```cpp #defineARRAY_SIZE(x)(sizeof(x)/sizeof((x)[0])) ``` ##Lvalue的作用 有兩個詞彙常令人誤解:`lvalue`(C和C++都有)和`rvalue`(C++特有)——兩者都是對運算式(expression)的分類方式,其存在可追溯到1960年代的[CPL程式語言](https://en.wikipedia.org/wiki/CPL_(programming_language))。

在[Valuecategories](https://en.cppreference.com/w/cpp/language/value_category)談及這部分的發展脈絡:CPL開始對運算式區分為左側模式(left-handmode)與右側模式(right-handmode),左、右是指運算式是在指定的左或右側,有些運算式只有在指定的左側才會有意義。

C語言有類似的分類方式,分為`lvalue`與其他運算式,似乎`l`暗示著left的首字母,不過C語言標準中明確聲明`l`是"locator"(定位器)的意思,lvalue定義為"locatorvalue",亦即lvalue是個物件的表示式(anexpressionreferringtoanobject),該物件的型態可以是一般的objecttype或incompletetype,但不可為`void`。

換句話說,運算式的結果會是個有名稱的物件。

到了C++98,非lvalue運算式被稱為`rvalue`,而部分在C語言隸屬「非lvalue的運算式」變成lvalue,後續在C++11,運算式又被重新分類為lvalue,prvalue,xvalue,lvalue與rvalue。

某些文件將lvalue和rvalue分別譯為左值、右值,這種翻譯方式顯然過時,且偏離C和C++語言規格的定義,我們要知道,lvalue和rvalue只是個分類名稱。

:::info 無論是C語言、B語言,還是Pascal語言,都可追溯到[ALGOL60](https://en.wikipedia.org/wiki/ALGOL_60),後者是AlgorithmicLanguage(演算法使用的語言)的縮寫,提出巢狀(nested)結構和一系列程式流程控制,今日我們熟知的if-else語法,就在ALGOL60出現。

ALGOL60和COBOL程式語言並列史上最早工業標準化的程式語言。

除了ALGOL60,直接影響C語言的程式語言是BCPL,後者是劍橋大學的MartinRichards於1967年訪問麻省理工學院時期所設計。

BCPL程式語言的提出,是回應同樣是劍橋大學發展的CPL(最初的意思是CambridgeProgrammingLanguage,後來由於和倫敦大學合作,更名為"CombinedProgrammingLanguage",也被戲稱為"CambridgePlusLondon")。

首篇提及CPL的論文發表於1963年,其設計接近硬體,程式設計的門檻高,而且不容易實作其編譯器,拖到1970年才有首個CPL的編譯器。

BCPL對CPL做了簡化,這個"B"字母就是"Basic"的意思,由於BCPL語言設計的精簡,所以其編譯器可在16KB的空間實作出來,也是最早[HelloWorld程式出現的程式語言](http://www.catb.org/jargon/html/B/BCPL.html)。

::: 在規格書的Rationale將lvalue定義為objectlocator(C99) *C996.3.2.1footnote >Thename"lvalue"comesoriginallyfromtheassignmentexpressionE1=E2,inwhichtheleftoperandE1isrequiredtobea(modifiable)lvalue.Itisperhapsbetterconsideredasrepresentinganobject"locatorvalue".Whatissometimescalled"rvalue"isinthisInternationalStandarddescribedasthe"valueofanexpression".Anobviousexampleofanlvalueisanidentifierofanobject.Asafurtherexample,ifEisaunaryexpressionthatisapointertoanobject,*EisanlvaluethatdesignatestheobjecttowhichEpoints. 舉例來說: ```cpp inta=10; int*E=&a; ++(*E);//a=a+1 ++(a++);//error ``` E就是上面C99註腳提及的"apointertoanobject"(這裡的object指的就是a佔用的記憶體空間),下面列舉E這個identifier不同角度所代表的身份: *`E` +object:儲存addressofintobject的區域 +lvalue:Eobject的位置,也就是Eobject這塊區域的address *`*E` +lvalue:對E這個object做dereference也就是把Eobject所代表的內容(addressofintobject)做dereference,也就得到了intobject的位置,換個說法就是上面所提到的lvaluethatdesignatestheobjectwhichEpoints 以gcc編譯上方程式碼會產生 >error:**lvalue**requiredasincrementoperand 錯誤訊息,因為`a++`會回傳a的value,而這個value是暫存值也就是個non-lvalue,而`++()`這個operator的operand必須要是一個lvalue,因為要寫回data,需要有地方(location)可寫入 *延伸閱讀:[UnderstandinglvaluesandrvaluesinCandC++](http://eli.thegreenplace.net/2011/12/15/understanding-lvalues-and-rvalues-in-c-and-c) 透過Lvalue解釋compoundliteral效果的[案例](http://stackoverflow.com/questions/14105645/is-it-possible-to-create-an-anonymous-initializer-in-c99/14105698#14105698): ```cpp #include structs{char*a;}; voidf(structs*p){printf("%s\n",p->a);} intmain(){ f(&(structs){"helloworld"}); return0; } ``` ##InsecurecodinginC -[slideshare](http://www.slideshare.net/olvemaudal/insecure-coding-in-c-and-c)by[OlveMaudal](http://www.slideshare.net/olvemaudal?utm_campaign=profiletracking&utm_medium=sssite&utm_source=ssslideview) -許多危險、未定義的C語言程式碼 -[MISRAC](https://en.wikipedia.org/wiki/MISRA_C) >"GuidelinesfortheuseoftheClanguageincriticalsystems" ##Nullpointer? -[台灣工程師常唸錯的英文單字](http://blog.privism.org/2012/06/blog-post.html) -null`[nʌl]`請不要發「怒偶」,他沒有怒。

![](https://hackpad-attachments.s3.amazonaws.com/embedded2015.hackpad.com_s0rlzR8wVtm_p.299401_1474171871668_undefined) >[source](https://www.facebook.com/photo.php?fbid=10206570192283054&set=p.10206570192283054) 考慮以下程式(null.c) ```cpp intmain(){return(NULL==0);} ``` 用gcc-7.2編譯: ```shell null.c:1:22:'NULL'undeclared(firstuseinthisfunction) intmain(){return(NULL==0);} ^~~~ null.c:1:22:note:eachundeclaredidentifierisreportedonlyonceforeachfunctionitappearsin ``` 表示NULL需要定義,我們加上`#include`即可通過編譯。

那`main`的回傳值是0還是1(或非零)呢? -依據C99規格6.3.2.3 -nothingrequirestheNULL**pointer**tohavethenumericvalueofzero; -這點導致實做上的歧異 -C99規格6.3.2.3-3 -A“null**pointer**constant”doesnothavetobea“null**pointer**” *[C++11-NULLPTR!=NULL](https://cppturkiye.wordpress.com/2016/02/05/c11-nullptr-null/) *[空指標常數(nullpointerconstant)](https://www.ptt.cc/bbs/C_and_CPP/M.1461840781.A.192.html) *依據C99/C11規格**6.3.2.3**-3 -"Anintegerconstantexpressionwiththevalue0,orsuchanexpressioncasttotypevoid*,iscalledanullpointerconstant.66)Ifanullpointerconstantisconvertedtoapointertype,theresultingpointer,calledanullpointer,isguaranteedtocompareunequaltoapointertoanyobjectorfunction." -在caller端轉換數值為pointertype時,就會變為nullpointer。

-延伸C99/C11規格**6.3.2.3**-4 -Conversionofanullpointertoanotherpointertypeyieldsanullpointerofthattype.Anytwonullpointersshallcompareequal. -(void*)(uintptr_t)0本質上是個nullpointer,而NULL是nullptr,以C99的觀點。

-C99規格6.7.8Initialization -當透過malloc()建立的物件經由free()釋放後,應該將指標設定為NULL -避免doubly-free -`free()`傳入NULL是安全的 [source](http://www.viva64.com/en/b/0306/)(裡頭有非常精彩的undefinedbehavior討論) ###兩個NULL指標是否能比較? -依據C99規格6.3.2.3-3 -"Anintegerconstantexpressionwiththevalue0,orsuchanexpressioncasttotypevoid*,iscalledanullpointerconstant.55)Ifanullpointerconstantisconvertedtoapointertype,theresultingpointer,calledanullpointer,isguaranteedtocompare**unequal**toapointertoanyobjectorfunction." -先解釋**_compareunequal_** -"compareunequal"意味著"a==bisfalse",換句話說:"a!=bistrue" -接下來C99規格在6.3.2.3又說: -Conversionofanullpointertoanotherpointertypeyieldsanullpointerofthattype.Anytwonullpointersshallcompareequal. -對pointertype作比較,又是另外的狀況 -依據C99規格6.5.8-5 -"Comparisonof**pointers**thatdonotpointtothesameobjectortomembersofthesameaggregateisundefined." -需要注意的是condition(傳入assert()的參數也是)依據C99規範是“ascalartype”,而pointers也是scalar ###C語言offsetof巨集的定義 ``定義了[offsetof巨集](https://en.wikipedia.org/wiki/Offsetof),取得struct特定成員(member)的位移量。

傳統的實做方式如下: ```cpp #defineoffsetof(st,m)((size_t)&(((st*)0)->m)) ``` 這對許多C編譯器(像是早期的gcc)可正確運作,但依據C99規格,這是undefinedbehavior。

後來的編譯器就不再透過巨集來實做,而改用builtinfunctions,像是gcc: ```cpp #defineoffsetof(st,m)__builtin_offsetof(st,m) ``` 這對於C++非常重要,否則原本的巨集連編譯都會失敗。

延伸閱讀:[C99的offsetofmacro](http://blog.linux.org.tw/~jserv/archives/001399.html) Linux核心延伸offsetof,提供container_of巨集,作為反向的操作,也就是給予成員位址和型態,反過來找struct開頭位址: ```cpp #definecontainer_of(ptr,type,member)({\ consttypeof(((type*)0)->member)*__mptr=(ptr);\ (type*)((char*)__mptr-offsetof(type,member));}) ``` 應用方式請見:[Linux核心原始程式碼巨集:`container_of`](https://hackmd.io/@sysprog/linux-macro-containerof) ##延伸閱讀 -[C語言常見誤解](https://docs.google.com/document/d/12cR0qb-kl7jB8RA_eHQu8OSmGKEOshwLeX4Mx5qBzUE/edit)byPTTlittleshan+Favonia -注意:部份以32-bit架構為探討對象 -建議把前面幾節閱讀完,再來讀這篇FAQ -漫談C語 -[豆瓣連結](http://www.douban.com/group/topic/55441892/)(需登入) -[作者blog](https://sunxiunan.com/archives/1661) -[CTrapsandPitfalls](http://www.literateprogramming.com/ctraps.pdf) -[TheDescenttoC](http://www.chiark.greenend.org.uk/~sgtatham/cdescent/) 33 × Signin Email Password Forgotpassword or Byclickingbelow,youagreetoourtermsofservice. SigninviaFacebook SigninviaTwitter SigninviaGitHub SigninviaDropbox SigninviaGoogle NewtoHackMD?Signup



請為這篇文章評分?