深入理解C語言小記

2022-02-06 08:46:57 字數 3969 閱讀 6584

首先,我們先來看下面這個經典的**:

int main()  

從這段**裡你看到了什麼問題?我們都知道,這段程式裡少了乙個#include 還少了乙個return 0;的返回語句。

不過,讓我們來深入的學習一下,

這段**在c++下無法編譯,因為c++需要明確宣告函式

這段**在c的編譯器下會編譯通過,因為在編譯期,編譯器會生成乙個printf的函式定義,並生成.o檔案,鏈結時,會找到標準的鏈結庫,所以能編譯通過。

但是,你知道這段程式的退出碼嗎?在ansi-c下,退出碼是一些未定義的垃圾數。但在c89下,退出碼是3,因為其取了printf的返回值。為什麼printf函式返回3呢?因為其輸出了』4′, 』2′,』\n』 三個字元。而在c99下,其會返回0,也就是成功地執行了這段程式。你可以使用gcc的 -std=c89或是-std=c99來編譯上面的程式看結果。

另外,我們還要注意main(),在c標準下,如果乙個函式不要引數,應該宣告成main(void),而main()其實相當於main(…),也就是說其可以有任意多的引數。

我們再來看一段**:

#include 

<

stdio.h

>

void f(void)  

int main(void)  

這個程式會輸出什麼?

我相信你對a的輸出相當有把握,就分別是4,5,6,因為那個靜態變數。

對於c呢,你應該也比較肯定,那是一堆亂數。

但是你可能不知道b的輸出會是什麼?答案是1,2,3。為什麼和c不一樣呢?因為,如果要初始化,每次呼叫函式裡,編譯器都要初始化函式棧空間,這太費效能了。但是c的編譯器會初始化靜態變數為0,因為這只是在啟動程式時的動作。

全域性變數同樣會被初始化。

說到全域性變數,你知道 靜態全域性變數和一般全域性變數的差別嗎?是的,對於static 的全域性變數,其對鏈結器不可以見,也就是說,這個變數只能在當前檔案中使用。

我們再來看乙個例子:

#include 

<

stdio.h

>

void foo(void)  

void bar(void)  

int main(void)  

你知道這段**會輸出什麼嗎?a) 乙個隨機值,b) 42。a 和 b都對(在「在函式外訪問區域性變數的乙個比喻」文中的最後給過這個例子),不過,你知道為什麼嗎?

如果你使用一般的編譯,會輸出42,因為我們的編譯器優化了函式的呼叫棧(重用了之前的棧),為的是更快,這沒有什麼***。反正你不初始化,他就是隨機值,既然是隨機值,什麼都無所謂。

但是,如果你的編譯開啟了**優化的開關,-o,這意味著,foo()函式的**會被優化成main()裡的乙個inline函式,也就是說沒有函式呼叫,就像巨集定義一樣。於是你會看到乙個隨機的垃圾數。

下面,我們再來看乙個示例:

#include 

<

stdio.h

>

int b(void)   

int c(void)   

int main(void)  

這段程式會輸出什麼?,你會說是,3,4,7。但是我想告訴你,這也有可能輸出,4,3,7。為什麼呢? 這是因為,在c/c++中,表達的評估次序是沒有標準定義的。編譯器可以正著來,也可以反著來,所以,不同的編譯器會有不同的輸出。你知道這個特性以後,你就知道這樣的程式是沒有可移植性的。

我們再來看看下面的這堆**,他們分別輸出什麼呢?

示例一

int a=

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

示例二

int a=

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

示例三

int a=

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

示例四

int a=

41; if (a++ 

<

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

示例五

int a=

41; aa

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

只有示例一,示例三,示例四輸出42,而示例二和五的行為則是未定義的。關於這種未定義的東西又叫sequence points,因為這會讓編譯器不知道在乙個表示式順列上如何訪問變數的值。比如a = a++,a + a++,不過,在c中,這樣的情況很少。

下面,再看一段**:(假設int為4位元組,char為1位元組)

struct x ;  

printf("%d,", sizeof(struct x));  

struct y ;  

printf("%d\n", sizeof(struct y));  

這個**會輸出什麼?

a) 9,10

b)12, 12

c)12, 16

答案是c,我想,你一定知道位元組對齊,是向4的倍數對齊。

但是,你知道為什麼要位元組對齊嗎?還是因為效能。因為這些東西都在記憶體裡,如果不對齊的話,我們的編譯器就要向記憶體乙個位元組乙個位元組的取,這樣一來,struct x,就需要取9次,太浪費效能了,而如果我一次取4個位元組,那麼我三次就搞定了。所以,這是為了效能的原因。

但是,為什麼struct y不向12 對齊,卻要向16對齊,因為char d; 被加在了最後,當編譯器計算乙個結構體的尺寸時,是邊計算,邊對齊的。也就是說,編譯器先看到了int,很好,4位元組,然後是 char,乙個位元組,而後面的int又不能填上還剩的3個位元組,不爽,把char b對齊成4,於是計算到d時,就是13 個位元組,於是就是16啦。但是如果換一下d和c的宣告位置,就是12了。

另外,再提一下,上述程式的printf中的%d並不好,因為,在64位下,sizeof的size_t是unsigned long,而32位下是 unsigned int,所以,c99引入了乙個專門給size_t用的%zu。這點需要注意。在64位平台下,c/c++ 的編譯需要注意很多事。你可以參看《64位平台c/c++開發注意事項》。

下面,我們再說說編譯器的warning,請看**:

#include 

<

stdio.h

>

int main(void)  

考慮下面兩種編譯**的方式 :

cc -wall a.c

cc -wall -o a.c

前一種是不會編譯出a未初化的警告資訊的,而只有在-o的情況下,再會有未初始化的警告資訊。這點就是為什麼我們在makefile裡的cflags上總是需要-wall和 -o。

最後,我們再來看乙個指標問題,你看下面的**:

#include 

<

stdio.h

>

int main(void)  

第一條printf語句應該沒有問題,就是 bfe2e100

第二條printf語句你可能會以為是bfe2e101。那就錯了,a+1,編譯器會編譯成 a+ 1*sizeof(int),int在32位下是4位元組,所以是加4,也就是bfe2e104

第三條printf語句可能是你最頭疼的,我們怎麼知道a的位址?我不知道嗎?可不就是bfe2e100。那豈不成了a==&a啦?這怎麼可能?自己存自己的?也許很多人會覺得指標和陣列是一回事,那麼你就錯了。如果是 int *a,那麼沒有問題,a == &a。但是這是陣列啊a,所以&a其實是被編譯成了 &a[0]。

第四條printf語句就很自然了,就是bfe2e114。

看過這麼多,你可能會覺得c語言設計得真拉淡啊。不過我要告訴下面幾點dennis當初設計c語言的初衷:

1)相信程式設計師,不阻止程式設計師做他們想做的事。

2)保持語言的簡潔,以及概念上的簡單。

3)保證效能,就算犧牲移植性。

今天很多語言進化得很高階了,語法也越來越複雜和強大,但是c語言依然光芒四射,dennis離世了,但是c語言的這些設計思路將永遠不朽。

深入理解C語言 深入理解指標

關於指標,其是c語言的重點,c語言學的好壞,其實就是指標學的好壞。其實指標並不複雜,學習指標,要正確的理解指標。指標也是一種變數,占有記憶體空間,用來儲存記憶體位址 指標就是告訴編譯器,開闢4個位元組的儲存空間 32位系統 無論是幾級指標都是一樣的 p操作記憶體 在指標宣告時,號表示所宣告的變數為指...

深入理解C語言 深入理解指標

關於指標,其是c語言的重點,c語言學的好壞,其實就是指標學的好壞。其實指標並不複雜,學習指標,要正確的理解指標。指標也是一種變數,占有記憶體空間,用來儲存記憶體位址 指標就是告訴編譯器,開闢4個位元組的儲存空間 32位系統 無論是幾級指標都是一樣的 p操作記憶體 在指標宣告時,號表示所宣告的變數為指...

C語言指標深入理解

前幾天看了乙個程式,裡面一段關於指標的 讓我非常糾結,看了很久才看懂,在這裡將將其記錄下來,希望能對大家有一定的幫助,先看示例程式 編譯器gcc include include include typedef struct list node list node,list,plist node st...