程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 【Deep C (and C++)】深入理解C/C++(2)

【Deep C (and C++)】深入理解C/C++(2)

編輯:C++入門知識

譯自Deep C (and C++) by Olve Maudal and Jon Jagger,本身半桶水不到,如果哪位網友發現有錯,留言指出吧:)

       

 好,接著深入理解C/C++之旅。我在翻譯第一篇的時候,自己是學到不不少東西,因此打算將這整個ppt翻譯完畢。

 

請看下面的代碼片段:


#include <stdio.h> 
 
void foo(void) 

    int a; 
    printf("%d\n", a); 

 
void bar(void) 

    int a = 42; 

 
int main(void) 

    bar(); 
    foo(); 


編譯運行,期待輸出什麼呢?


$  cc  foo.c  &&  ./a.out 
42 

你可以解釋一下,為什麼這樣嗎?

第一個候選者:嗯?也許編譯器為了重用有一個變量名稱池。比如說,在bar函數中,使用並且釋放了變量a,當foo函數需要一個整型變量a的時候,它將得到和bar函數中的a的同一內存區域。如果你在bar函數中重新命名變量a,我不覺得你會得到42的輸出。

你:恩。確定。。。

 

第二個候選者:不錯,我喜歡。你是不是希望我解釋一下關於執行堆棧或是活動幀(activation frames, 操作代碼在內存中的存放形式,譬如在某些系統上,一個函數在內存中以這種形式存在:

ESP

形式參數

局部變量

EIP

)?

你:我想你已經證明了你理解這個問題的關鍵所在。但是,如果我們編譯的時候,采用優化參數,或是使用別的編譯器來編譯,你覺得會發生什麼?

候選者:如果編譯優化措施參與進來,很多事情可能會發生,比如說,bar函數可能會被忽略,因為它沒有產生任何作用。同時,如果foo函數會被inline,這樣就沒有函數調用了,那我也不感到奇怪。但是由於foo函數必須對編譯器可見,所以foo函數的目標文件會被創建,以便其他的目標文件鏈接階段需要鏈接foo函數。總之,如果我使用編譯優化的話,應該會得到其他不同的值。


$  cc -O foo.c  &&  ./a.out 
1606415608 
候選者:垃圾值。

 

那麼,請問,這段代碼會輸出什麼?


#include <stdio.h> 
  
void foo(void) 

   int a = 41; 
    a= a++; 
   printf("%d\n", a); 

  
int main(void) 

   foo(); 

第一個候選者:我沒這樣寫過代碼。

你:不錯,好習慣。

候選者:但是我猜測答案是42.

你:為什麼?

候選者:因為沒有別的可能了。

你:確實,在我的機器上運行,確實得到了42.

候選者:對吧,嘿嘿。

你:但是這段代碼,事實上屬於未定義。

候選者:對,我告訴過你,我沒這樣寫過代碼。

 

第二個候選者登場:a會得到一個未定義的值。

你:我沒有得到任何的警告信息,並且我得到了42.

候選者:那麼你需要提高你的警告級別。在經過賦值和自增以後,a的值確實未定義,因為你違反了C/C++語言的根本原則中的一條,這條規則主要針對執行順序(sequencing)的。C/C++規定,在一個序列操作中,對每一個變量,你僅僅可以更新一次。這裡,a = a++;更新了兩次,這樣操作會導致a是一個未定義的值。

你:你的意思是,我會得到一個任意值?但是我確實得到了42.

候選者:確實,a可以是42,41,43,0,1099,或是任意值。你的機器得到42,我一點都不感到奇怪,這裡還可以得到什麼?或是編譯前選擇42作為一個未定義的值:)呵呵:)

 

那麼,下面這段代碼呢?


#include <stdio.h> 
  
int b(void) 

   puts("3"); 
   return 3; 

  
int c(void) 

   puts("4"); 
   return 4; 

  
int main(void) 

   int a = b() + c(); 
   printf("%d\n", a); 

 
第一個候選者:簡單,會依次打印3,4,7.

你:確實。但是也有可能是4,3,7.

候選者:啊?運算次序也是未定義?

你:准確的說,這不是未定義,而是未指定。

候選者:不管怎樣,討厭的編譯器。我覺得他應該給我們警告信息。

你心裡默念:警告什麼?

 

第二個候選者:在C/C++中,運算次序是未指定的,對於具體的平台,由於優化的需要,編譯器可以決定運算順序,這又和執行順序有關。

         這段代碼是符合C標准的。這段代碼或是輸出3,4,7或是輸出4,3,7,這個取決於編譯器。

你心裡默念:要是我的大部分同事都像你這樣理解他們所使用的語言,生活會多麼美好:)

 

這個時候,我們會覺得第二個候選者對於C語言的理解,明顯深刻於第一個候選者。如果你回答以上問題,你停留在什麼階段?:)

 

那麼,試著看看第二個候選者的潛能?看看他到底有多了解C/C++

 

可以考察一下相關的知識:

聲明和定義;

調用約定和活動幀;

序點;

內存模型;

優化;

不同C標准之間的區別;

 

 

這裡,我們先分享序點以及不同C標准之間的區別相關的知識。

 

考慮以下這段代碼,將會得到什麼輸出?


1. 
int a = 41; 
a++; 
printf("%d\n", a); 
答案:42 
  
2. 
int a = 41; 
a++ & printf("%d\n", a); 
答案:未定義 
  
3. 
int a = 41; 
a++ && printf("%d\n", a); 
答案:42 
  
4. int a = 41; 
if (a++ < 42) printf("%d\n",a); 
答案:42 
  
5. 
int a = 41; 
a = a++; 
printf("%d\n", a); 
答案:未定義 

到底什麼時候,C/C++語言會有副作用?

序點:

什麼是序點?

簡而言之,序點就是這麼一個位置,在它之前所有的副作用已經發生,在它之後的所有副作用仍未開始,而兩個序點之間所有的表達式或者代碼執行的順序是未定義的!

 \

序點規則1:

在前一個序點和後一個序點之前,也就是兩個序點之間,一個值最多只能被寫一次;

 

這裡,在兩個序點之間,a被寫了兩次,因此,這種行為屬於未定義。

\

序點規則2:

進一步說,先前的值應該是只讀的,以便決定要存儲什麼值。

 \

很多開發者會覺得C語言有很多序點,事實上,C語言的序點非常少。這會給編譯器更大的優化空間。

 

接下來看看,各種C標准之間的差別:

 \

現在讓我們回到開始那兩位候選者。

 

下面這段代碼,會輸出什麼?


#include <stdio.h> 
  
struct X 

   int a; 
   char b; 
   int c; 
}; 
  
int main(void) 

   printf("%d\n", sizeof(int)); 
   printf("%d\n", sizeof(char)); 
   printf("%d\n", sizeof(struct X)); 

第一個候選者:它將打印出4,1,12.

你:確實,在我的機器上得到了這個結果。

候選者:當然。因為sizeof返回字節數,在32位機器上,C語言的int類型是32位,或是4個字節。char類型是一個字節長度。在struct中,本例會以4字節來對齊。

你:好。

你心裡默念:do you want another ice cream?(不知道有什麼特別情緒)

 

第二個候選者:恩。首先,先完善一下代碼。sizeof的返回值類型是site_t,並不總是與int類型一樣。因此,printf中的輸出格式%d,不是一個很好的說明符。

你:好。那麼,應該使用什麼格式說明符?

候選者:這有點復雜。site_t是一個無符號整型數,在32位機器上,它通常是一個無符號的int類型的數,但是在64位機器上,它通常是一個無符號的long類型的數。然而,在C99中,針對site_t類型,指定了一個新的說明符,所以,%zu會是一個不多的選擇。

你:好。那我們先完善這個說明符的bug。你接著回答這個問題吧。


#include <stdio.h> 
  
struct X 

   int a; 
   char b; 
   int c; 
}; 
  
int main(void) 

   printf("%zu\n", sizeof(int)); 
   printf("%zu\n", sizeof(char)); 
   printf("%zu\n", sizeof(struct X)); 

  

候選者:這取決與平台,以及編譯時的選項。唯一可以確定的是,sizeof(char)是1.你要假設在64位機器上運行嗎?
你:是的。我有一台64位的機器,運行在32位兼容模式下。

候選者:那麼由於字節對齊的原因,我覺得答案應該是4,1,12.當然,這也取決於你的編譯選項參數,它可能是4,1,9.如果你在使用gcc編譯的時候,加上-fpack-struct,來明確要求編譯器壓縮struct的話。

你:在我的機器上確實得到了4,1,12。為什麼是12呢?

候選者:工作在字節不對齊的情況下,代價非常昂貴。因此編譯器會優化數據的存放,使得每一個數據域都以字邊界開始存放。struct的存放也會考慮字節對齊的情況。

你:為什麼工作在字節不對齊的情況下,代價會很昂貴?

候選者:大多數處理器的指令集都在從內存到cpu拷貝一個字長的數據方面做了優化。如果你需要改變一個橫跨字邊界的值,你需要讀取兩個字,屏蔽掉其他值,然後改變再寫回。可能慢了10不止。記住,C語言很注意運行速度。

你:如果我得struct上加一個char d,會怎麼樣?

候選者:如果你把char d加在struct的後面,我預計sizeof(struct X)會是16.因為,如果你得到一個長度為13字節的結構體,貌似不是一個很有效的長度。但是,如果你把char d加在char  b的後面,那麼12會是一個更為合理的答案。

你:為什麼編譯器不重排結構體中的數據順序,以便更好的優化內存使用和運行速度?

候選者:確實有一些語言這樣做了,但是C/C++沒有這樣做。

你:如果我在結構體的後面加上char *d,會怎麼樣?

候選者:你剛才說你的運行時環境是64位,因此一個指針的長度的8個字節。也許struct的長度是20?但是另一種可能是,64位的指針需要在在效率上對齊,因此,代碼可能會輸出4,1,24?

你:不錯。我不關心在我的機器上會得到什麼結果,但是我喜歡你的觀點以及洞察力J

(未完待續)

靜候

摘自 Rockics的專欄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved