天天看點

C語言易混淆關鍵詞詳解-const, static, extern, typedef, 聲明

const

const關鍵詞并不能把一個變量變成一個常量, 在符号前加上const表示這個符号不能被指派, 即他的值對這個符号來說是隻讀的, 但并不代表這個值不能用其他方法去改變. 通過下面的例子就能比較好了解,

int i = 5;

const int *a = &i;

*a = 8;   //報錯, 隻讀不能指派

i = 10;   //ok

const最有用處的地方是用它來限定函數的形參, 來表明該函數不能修改實參指針所指向的資料. 同上面的了解, 并不表示這個資料是常量, 在函數外是可以修改的. 如void func(const char *) 

const出現的位置也比較讓人困惑, c太靈活的壞處

char *p              = "hello";          // 非const指針, 非const資料

const char *p        = "hello";          //非const指針 const資料

char * const p       = "hello";          // const指針,非const資料

const char * const p = "hello";          // const指針,const資料

你可以在頭腦裡畫一條垂直線穿過指針聲明中的星号(*)位置,如果const出現線上的左邊,指針指向的資料為常量;如果const出現線上的右邊,指針本身為常量;如果const線上的兩邊都出現,二者都是常量

static 

c語言 中,

static 局部變量 , 生存期為這個源程式, 不過作用域仍難是局部

int fun()

{

     static int a = 1;

     a++;

     print('%d',a);

}

隻有第一次調用該函數時a會被初始化為1, 後面每次調用a都會增加1, 是以隻要程式不結束這個static a是一直存在的

但他是局部變量, 是以在fun函數之外無法通路, 雖然static a 一直存在

static 全局變量

全局變量本身就是靜态存儲方式, 再加上static, 是改變他的作用域, 即隻能本目前檔案通路. 而非static 全局變量的作用域為整個源程式

是以對局部變量, static改變的是他的生存期, 而對于全局變量, static改變的是他的作用域

對于c這樣用相同關鍵詞, 卻用做完全不同的用處, 真是無法了解, 簡直是在忽悠使用者

static 函數

在c語言中, 函數的預設作用域是全局可見的, 即整個源程式, 你也可以給函數加上個備援的extern, 來表示其作用域

如果在函數前加上static, 表示将其作用域縮小至本檔案, 同于靜态全局變量的用法.

此處普遍認為是c語言的設計失誤, 不應該預設将作用域設為全局, 容易造成命名空間沖突.

c++ 中

c++中除了c中的用法, 還多了static成員變量, 和static成員函數的用法

表示屬于一個類而不是屬于此類的任何特定對象的變量和函數. 這是與普通成員函數的最大差別, 也是其應用所在, 比如在對某一個類的對象進行計數時, 計數生成多少個類的執行個體, 就可以用到靜态資料成員.

在這裡面, static既不是限定作用域的, 也不是擴充生存期的作用, 而是訓示變量/函數在此類中的唯一性. 這也是”屬于一個類而不是屬于此類的任何特定對象的變量和函數”的含義.

是以對靜态成員的引用不需要用對象名, 可以直接使用類名,

靜态成員函數僅能通路靜态的資料成員,不能通路非靜态的資料成員,也不能通路非靜态的成員函數,這是由于靜态的成員函數沒有this指針

extern

參考自(http://blog.csdn.net/keensword/archive/2005/06/23/401114.aspx)

如果想明白為什麼需要extern, 需要從編譯和連結讨論起,

現代編譯器一般采用按檔案編譯的方式,是以在編譯時,各個檔案中定義的全局變量是互相透明的,也就是說,在編譯時,全局變量的可見域限制在檔案内部。但是到了連結階段,要将各個檔案的内容“合為一體”,是以,如果某些檔案中定義的全局變量名相同的話,會報錯. 是以,各個檔案中定義的全局變量名不可相同。

//a.cpp

int i;

void main()

//b.cpp

是以上面兩個檔案編譯是沒有問題的, 但是到了連結就會報重名錯誤

如果此時a.cpp裡面要用到, b.cpp中定義的i, 應該怎麼辦?

那麼既然上面說了重複定義出錯, 那就把a.cpp中的"int i;"定義直接去掉是否可以

看起來好像可以的, 因為全局變量的作用域是整個源程式, 這邊也許是很多人會産生疑問的地方, 既然全局變量和全局函數的作用域是整個源程式的, 為什麼在其他的檔案裡面使用一定要先聲明(這樣的聲明往往在.h檔案中, 并在使用處include該.h, 記住include就是copy, 之是以要使用.h, 而不是直接寫在.c中, 隻是為了保證易維護性, 最終編譯器會自動将.h copy到每個.c中)

答案就在編譯階段, 過程是先編譯後連結, 而在編譯時隻能知道本檔案的内容, 編譯器并不能預見到你這個變量或函數在其他檔案裡面被定義過. 隻有到了連結階段編譯器産能看到其他的檔案.

是以如果不事先聲明, 那麼在編譯階段一定會報錯找不到該變量或函數.

extern的原理很簡單,就是告訴編譯器:“你現在編譯的檔案中,有一個辨別符雖然沒有在本檔案中定義,但是它是在别的檔案中定義的全局變量,你要放行!”

多說一句, 在聲明變量是必須要加extern, 而在聲明函數時卻不需要, 為什麼

上面說了, 聲明隻是簡單的告訴編譯器, 這個東西在其他地方定義過了, 你不用管了, 是以編譯器不會為聲明配置設定空間, 或做其他操作, 這和定義是有本質差別的, 必須要正确區分

對于變量必須用extern才能區分定義和聲明, 因為這是變量定義和聲明的唯一差別

而函數不需要extern也能區分處定義和聲明, 有實作就是定義, 沒有就是聲明, 是以不需要再加extern

這就是c的簡潔之處, 不需要的就别寫

再多說一句, 在c中, 全局變量和函數都是預設對外可見的, 如果想變成僅目前檔案可見, 必須加上static.

對于函數, 預設和加上extern是等價的, 都是表示對外可見

但是對于變量, 确不一樣, 加上extern就變成聲明了, 是以不能給定義加上extern

是以對于extern有如下說法,

用于變量,聲明該變量在其它地方定義;

用于函數定義, 表示全局可見(屬于備援的)

總覺得c語言的設計者是在玩程式員, 不把你繞進去, 他不爽. 你不能把每個程式員都想的和你智商一樣高啊...

extern“c”

extern "c" 包含雙重含義,從字面上即可得到:首先,被它修飾的目标是“extern”的;其次,被它修飾的目标是“c”的。

extern的意思通過上面的解釋, 應該是很明白了, 那麼c什麼意思

這個就要談起c和c++的混合編譯, 重要的是c和c++編譯器對變量名的改寫方式是不同的

對于c編譯器, 往往是在變量名和函數名之前統一加上了一個下劃線

int func(int t)    >>>   public    _func

而對于c++編譯器, 則要複雜的多, 因為c++中有函數重載等, 允許相同的函數名有不同的參數, 和不同的作用域, 是以使用name mangling來唯一辨別每個函數, 比如上面的函數,  被編譯成了func@@yahh@z

//a.cpp 

void func();

         func();

//b.c 

void func()

是以象上面c和c++檔案的混合編譯, 就會報錯, a.obj : error lnk2001: unresolved external symbol "void __cdecl func(void)" (?func@@yaxxz)

這個錯以前經常看到的說...

為什麼會報這個錯了, 因為你在a.cpp編譯是聲明了func, 是以編譯通過, 然後在連結的時候, 編譯器就會去找這個func函數, 因為這個是c++編譯器, 是以他就是去找func@@yahh@z, 結果沒找到, 編譯器發現被騙了...于是不幹了

為啥找不到了, 因為你下面的b.c是用c編譯器編譯的, 是以生成的函數名是_func, 而不是 func@@yahh@z, 是以發生這個情況

你把a.cpp中的聲明改成這樣就可以了, 明确告訴c++編譯器, 這個函數的名字不要亂改, 還是用c的方式, 這樣就能找到了

extern "c"

    void func();

    func();

補充一下, 這個問題對于全局變量一樣存在, 在c++中調用c中的全局變量一樣要加 extern "c", 來限制name mangling.

struct union enum

struct

在c中結構的定義是這樣的

struct optional_tag {

    type_1 identifier_1;

    type_2 identifier_2;

    ...

    type_n identifier_n;

} optional_variable_definitions;

so with the declarations

struct date_tag { short dd,mm,yy; } my_birthday, xmas;

struct date_tag easter, groundhog_day;

variables my_birthday, xmas, easter, and groundhog_day all have the identical type.

在結構中允許出現位段, 無名段, 填充段

struct pid_tag {

unsigned int inactive :1;

unsigned int :1;                 /* 1 bit of padding */

unsigned int refcount :6;

unsigned int :0;                 /* pad to next word boundary*/

short pid_id;

struct pid_tag *link;

};

this is commonly used for "programming right down to the silicon," and you'll see it in systems programs. it can also be used for storing aboolean flag in a bit rather than a char . a bit field must have a type of int, unsigned int, or signed int (or a qualified version of one of these).

下面給兩個struct的比較有意思的用法,

數組copy

int a[100], b[100];

如果這時想将b數組這個copy到a數組, 或把數組作為參數或傳回值(雖然這樣不常用, 一般用指針)

比較簡單的辦法, 是把數組分裝到struct中

struct s_tag { int a[100]; };

struct s_tag orange, lime, lemon;

//先初始化lemon

orange = lemon; /* assigns entire struct */

結構體常用來實作連結清單, tree之類的資料結構

struct node_tag { int datum;

                             struct node_tag *next;

struct node_tag a,b;

a.next = &b;          /* example link-up */

a.next->next=null;

位元組對齊

說到結構, 順便談一下位元組對齊

計算機從存儲器上讀取資料的時候是以機器字為機關的, 機器字的大小取決于計算機本身的處理位數, 最常見的32位機, 機器字就是32位, 即4位元組. 這個是合理的, 因為cpu的處理機關是一次32位, 是以必然一次也讀32位, 多讀了也處理不了.

既然一次讀4位元組, 是以從存儲器上讀取資料的時候, 隻會從能被4整除的位元組位址開始讀, 即隻能從機器字起始位置開始讀

這樣有個問題, 一般讀取一個int, 隻需要一個讀周期, 因為int就是4位元組, 剛好可以一個讀周期被讀到, 但是問題在于不能保證int存儲的首位址是4的倍數, 也就是說這個int的存儲跨越了2個機器字, 這樣通過一個讀周期就無法讀出這個int了, 必須要兩個讀周期讀出2個機器字, 然後拼出這個int來.

那麼這樣明顯是低效的, 浪費了很多讀指令周期, 那麼要解決這個問題, 簡單的方法就是盡量讓資料存放在更少的機器字中, 即4位元組對齊

先給出常用類型的位元組大小

char                      在位元組邊界上對齊             n=1

short (16-bit)            在雙位元組邊界上對齊           n=2

int (32-bit)              在4位元組邊界上對齊            n=4

long (32-bit)             在4位元組邊界上對齊            n=4

float                     在4位元組邊界上對齊            n=4

double                    在8位元組邊界上對齊            n=8

下面的例子就給出了4位元組對齊

struct {char a; int b;} t1;

sizeof(t1) == 8; n = 4 中間填充了3位元組

對于上面的例子, 一般的人可能認為size應該為5, 可是其實使用了8

那是因為編譯器考慮4位元組對齊, 在char後自動填充了3個位元組

由于編譯器的不同,對于四位元組對齊的定義就不同,有的編譯器會自動補齊成四位元組,有的不會。這樣會造成交叉編譯時不相容。是以在設計資料結構時,應該盡量設計成4位元組的倍數。

struct {char a; char b;} t;

sizeof(t) == 2

上面這樣的寫法不太好, 應該習慣把他補齊

改為, struct {char a; char b;short pad ;} t;

注意調整結構體内的資料順序可以有效的節省存儲空間

struct {char a ; int i; char b;} t1;

struct {char a; char b; int i;} t2;

sizeof(t1)==12  

sizeof(t2)==8

union

union和struct的定義一樣的, 隻是把struct換成union

但不同的是對于union來說, 所有的成員都是從偏移位址0開始存儲, 即是重合的, 同一時間隻能有一個成員真正存在, 而union的size就是成員中最大size.

union一般用來節省空間, 結構中可能有些成員是不會同時出現的, 就把他封裝在一個union中, 以節省空間

union其他用途比如可以把同一個資料做不同解釋,

union bits32_tag {

    int whole; /* one 32-bit value*/

    struct {char c0,c1,c2,c3;} byte; /* four 8-bit bytes*/

} value;

你即可以用value.whole取整個32bit int, 也可以用value.byte.c0取前8bit char

enum

enum的作用就是把一串名字和一串整型聯系在一起, 可以說在c中enum完全可以被#define取代, 比較雞肋的

enum sizes { small=7, medium, large=10, humungous };

如果不指派, 會從0開始遞增, 或從賦的值開始遞增

enum一個優點, 便于debug

there is one advantage to enums: unlike #defined names which are typically discarded during compilation, enum names usually persist through to the debugger, and can be used while debugging your code.

c語言聲明的解析

這個純粹出于理論研究, 實際開發中, 如果寫出這樣需要看半天才明白的聲明, 真是......

想正确解析c語言的聲明先記住如下的優先級

優先級規則如下:

a 聲明從它的名字 開始讀取,然後按照優先級順序依次讀取;

b 優先級從高到低依次是:

        b.1 聲明中被括号括起來 的那部分

        b.2 字尾 操作符;

             括号()表示這是一個函數,而

             方括号[] 表示這是一個數組。

        b.3 字首 操作符; 星号 * 表示“指向...的指針”。

c 如果const 和 ( 或 ) volatile 關鍵字的後面緊跟類型說明符 (如 int, long等),那麼它作用于類型說明符。在其他情況下,const 和 ( 或 ) volatile 關鍵字作用于它左邊緊鄰的指針星号。

然後來個例子, char * const * ( *next )( );

分析過程:

a、 首先,看變量名"next", 并注意到它直接被括号所包覆;

b.1、是以先把括号裡的東西作為一個整體,得出“next 是一個指向 ...的指針"。

b、 然後考慮括号外面的東西,在星号字首和括号字尾之間做出選擇。

b.2、規則告訴我們優先級較高的是右邊的函數括号,是以得出”next是一個函數指針,指向一個傳回...的函數”。

b.3、然後,處理字首“*”,得出指針所指的内容。

c、 最後,把"char * const" 解釋為指向字元的常量指針。

把上述分析結果加以概括,這個聲明表示“next是一個指針,它指向一個函數,該函數傳回另一個指針,該指針指向一個類型為char 的常量指針”,大功告成。

下面給出幾個比較難看懂的聲明

函數的傳回值是一個函數指針:              int (* fun())();

函數的傳回值是一個指向int數組的指針 : int (* foo())[]

數組的元素為函數指針:                        int (*foo[])()

數組的元素為數組, 多元數組:                int foo[][]

拿 int (* foo())[]詳細分析一下

首先從左邊找到第一個變量名foo, 明确foo是個函數, 而不是個指針 , 這點很重要, 因為()的優先級高于*

确定foo是函數後, 前面的*就表示foo的傳回值為指針

括号外[]表示傳回值是數組的指針

最後int表示 傳回值是一個指向int數組的指針

多說一下, 如果int (*(* foo)())[], foo就是個指針, 因為加上了()

typedef

typedef為類型引入新的名字, 而不是為變量配置設定空間, 某些方面typedef類似于宏文本替換, 不過他們是有些不同的

在程式設計中使用typedef目的一般有兩個,一個是給變量一個易記且意義明确的新名字,另一個是簡化一些比較複雜的類型聲明。

和#define的差別

1.typedef是一種徹底的"封裝"類型, 一旦聲明不能再增加.

#define peach int

unsigned peach i; /* works fine */

typedef int banana;

unsigned banana i; /* bzzzt! illegal */

對于#define隻是單純的文本替換, 是以在前面加上unsigned是可以的

而typedef就不可以, 編譯器會認為你的文法不合法, 就象你寫 float int i; 一樣

2. typedef能保證聲明中所有的變量類型一緻

這點上比#define要強

#define int_ptr int *

int_ptr chalk, cheese;

typedef char * char_ptr;

char_ptr bentley, rolls_royce;

這兒宏就是文本替換, 結果就是int * chalk, cheese; cheese被聲明為int, 而非int *

而typedef就可以保證bentley, rolls_royce都是char *

typedef的用法

不要為了省去寫struct, 而對結構使用typedef, struct會給閱讀code的人一些提示, 不應該省掉它

注意下面兩句, 很容易混淆...nnd...c對于程式員就是噩夢

typedef struct fruit {int weight, price_per_lb } frt; //将struct fruit命名為frt

struct fruit {int weight, price_per_lb } apple;//定義struct fruit, 并建立struct fruit類型的變量 apple

這就是上面說的情況, 用typedef隻是在建立變量時, 省去不用寫struct

struct fruit lemon;

frt lemon;

雖然很多地方都看到這樣使用typedef, 但這種用法不推薦 

typedef應該用在:

參考http://www.cnblogs.com/csyisong/archive/2009/01/09/1372363.html

1. 數組, 結構, 指針以及函數的組合類型

為複雜的聲明定義一個新的簡單的别名。方法是:在原來的聲明裡逐漸用别名替換一部分複雜聲明,如此循環,把帶變量名的部分留到最後替換,得到的就是原聲明的最簡化版。舉例:

原聲明:void (*b[10]) (void (*)()); 

變量名為b,先替換右邊部分括号裡的,pfunparam為别名一:

typedef void (*pfunparam)();

再替換左邊的變量b,pfunx為别名二:

typedef void (*pfunx)(pfunparam);

原聲明的最簡化版:

pfunx b[10];

原聲明:doube(*)() (*e)[9];

變量名為e,先替換左邊部分,pfuny為别名一:

typedef double(*pfuny)();

再替換右邊的變量e,pfunparamy為别名二

typedef pfuny (*pfunparamy)[9];

pfunparamy e;

2. 可移植類型 .

比如定義一個叫 real 的浮點類型,在目标平台一上,讓它表示最高精度的類型為:

typedef long double real;

在不支援 long double 的平台二上,改為:

typedef double real;

在連 double 都不支援的平台三上,改為:

typedef float real;

也就是說,當跨平台時,隻要改下 typedef 本身就行,不用對其他源碼做任何修改。

标準庫就廣泛使用了這個技巧,比如size_t。另外,因為typedef是定義了一種類型的新别名,不是簡單的字元串替換,是以它比宏來得穩健

3. 為後面的強制類型轉化 提供一個簡單的名字

typedef int (*ptr_to_int_fun)(void);

char * p;

(ptr_to_int_fun) p;

這邊我們拿void (*b[10]) (void (*)());來詳細分析一下

首先确認隻是個數組b, size為10, 數組的元素是函數指針(無傳回值, 參數為無傳回值的函數指針)

其實你了解這個聲明的意思, 就很容易簡化了

是以首先為無傳回值的函數指針定義一個别名叫pfunparam

當時我看到這個定義, 很疑惑, 一般看到的typedef都是類似, typedef double real; 

typedef後面兩個參數, 把double稱為real

但是這邊就一個函數指針, 啥意思

其實你可以看成typedef void (*)() pfunparam;

但是你直接這樣寫, 會報錯

我猜想對于這樣的複雜組合類型, 隻能寫成這樣的形式, 忍不住又要控訴c語言......太晦澀了

好繼續, 再為函數指針(無傳回值, 參數為無傳回值的函數指針)定義一個别名pfunx

好了, 你現在就把pfunx當做是int一樣去定義數組

本文章摘自部落格園,原文釋出日期:2011-07-05

繼續閱讀