C語言筆記

C語言筆記

變量相關:

  • C99以前的C要求在一個代碼塊的開始處聲明變量。
  • 聲明語句爲變量創建,標定存儲空間併爲其制定初始值。
  • C99引入_Bool類型表示布爾值。它還提供stdbool.h頭文件,包含這個頭文件可以使用bool來代替_Bool,並把true和false定義爲1和0的符號常量。
  • C99提供一個可選的名字集合,用來描述確定的位數。如int16_t表示一個16位有符號整數類型,uint32_t表示一個32位無符號整數類型。使用這些名字,需要包含頭文件inttypes.h。另外,這種確切的長度類型在某些系統上可能不支持。不如,不能保證某些系統上存在一種int8_t類型(8位有符號整數)。爲了解決這個問題,C99標準定義了第二組名字集合。這些名字保證所表示的類型至少大於指定長度的最小類型,被稱爲“最小長度類型”。例如,int_least8_t是可以容納8位有符號數的那些類型中長度最小的一個別名。因爲一些程序員更關心速度而非空間。C99爲他們定義了一組可使計算達到最快的類型集合。這組集合被稱爲“最快最小長度類型”。如,int_fast8_t定義爲系統中對8位有符號數而言計算最快的整數類型的別名。爲了使用printf輸出這些類型,C99還提供了一些串宏來幫助打印這些類型。(注:可能一些編譯器並不支持這一特性)
  • 浮點數的上溢和下溢。當一個計算結果是一個大的不能表達的數時,會發生上溢,現在C語言用一個特殊值表示這個太大的數,printf函數顯示此值爲inf或infinity。如果將浮點數能表示的最小的數除以2,將會得到一個低於正常的值,如果除以一個足夠大的數,將使所有位都爲0。現在C庫提供了用於檢查計算是否會產生低於正常的值的函數。有一個特殊的浮點值NaN(Not-a-Number)。例如asin函數返回反正弦值,但是正弦值不能大於1,所以它的輸入參數不能大於1,否則函數返回NaN值,printf函數顯示此值爲nan,NaN或類似形式。
  • C99還支持複數和虛數類型。
  • 在將浮點數轉換爲整數時,C簡單的丟棄小數部分(截尾),而不進行四捨五入。
  • sizeof運算符返回一個size_t類型的數,這個類型通常是unsigned或unsigned long。對於某個具體量的大小sizeof的括號是可選的,但對於類型來說,括號是必需的。如sizeof(int),sizeof(2.0)<=>sizeof 2.0。
  • C99標準要求編譯器識別局部標識符的前63個字符和外部標識符的前31個字符。在這之前分別爲31個和6個字符。

參數傳遞機制

對於下面的代碼段:

float n1 = 3.0;
double n2 = 3.0;
long n3 = 300000;
long n4 = 123456790;
printf("%ld %ld %ld %ld\n", n1, n2, n3, n4);

該調用告訴計算機把變量n1,n2,n3,n4的值傳遞給計算機,計算機把他們放置到被稱爲堆棧(stack)的一塊內存區域中來實現。計算機根據變量的類型而非轉換說明符把這些值放到堆棧中。所以,n1,n2,n3,n4四個變量在堆棧中分別佔8,8,4,4個字節,其中n1的float被轉換成了double。然後,控制交給了printf函數。該函數中堆棧中把值讀出來,但是在讀取時,它根據轉換說明符去讀取。%ld說明符指出,printf應該讀取4個字節,所以printf在堆棧中讀取前4個字節作爲它的一個值。這就是n1的前半部分,它被解釋成一個long類型。寫一個%ld說明符再讀取4個字節;這就是n1的後半部分,它也被解釋成一個long類型。同樣,第三個和第四個%ld說明符分別讀取n2的前半部分和後半部分,並解釋成一個long類型。所以雖然n3和n4的說明符都正確,但是printf仍然讀取了錯誤的字節。
一般來說,棧從高地址向低地址生長,即高地址是棧的底部。而函數的入棧順序是從右向左。從右向左入棧的原因是,這樣可以使得編譯器可以支持可變參數,通過第一個參數可以獲得參數的個數和每個參數的大小,這樣就能取得每個參數。
在C中,會發生許多自動類型轉換。當char和short類型出現在表達式裏或者作爲函數的參數時,它們都被提升爲int類型。當float類型作爲一個函數參數時被提升爲double。
在包含兩種數據類型的任何運算裏,兩個值都被轉換成兩種類型裏較高的級別。

優先級和求值順序

  1. 運算符的優先級爲決定表達式裏求值的順序提供了重要的規則,但是它並不決定所有的規則。如:
y = 6 * 12 + 5 * 20;

當兩個運算符共享一個操作數的時候,優先級規定了求值順序。例如,12既是*運算符的操作數,又是+運算符的操作數,根據優先級的規定乘法運算先進行。與之類似,優先級規定了對5進行乘法操作而不是加法操作。總之,兩個乘法操作在加法操作之前進行。但是優先級並沒有確定的是這兩個乘法運算中到底那個先進行。C將這個選擇權留給實現者,這是因爲可能一種選擇在一種硬件上效率更高,而另一種選擇在另一種硬件上效率更高。但是不管先執行那個乘法運算,表達式都會簡化成72+100。雖然乘法運算符的結合性是從左到右,但因爲兩個*運算符不共享一個操作數,所以從左到右的規則對它並不適用。也就是說結合規則適用於共享同一操作數的運算符。

  1. 對於參數傳遞,參數的求值順序也是不確定的。所以,下邊的語句的結果在不同的系統上可能會產生不同的結果。
int num = 1;
printf("%d %d\n", num, num * num++);
int n = 2;
printf("%d\n", n++ + n++);

但是我們可以通過一下原則來避免這些問題。

  • 如果一個變量出現在同一個函數的多個參數中,不要將增量或減量運算符用於它上邊。
  • 當一個變量多次出現在一個表達式裏時,不要將增量或減量運算符用於它上邊。

順序點是程序執行中的一點,在該點處,所有的副作用都在進入下一步之前被計算。在C中語句裏的分號標誌了一個順序點。任何一個完整的表達式結束也是一個順序點。一個完整的表達式是這樣一個表達式——它不是一個更大的表達式的子表達式。逗號運算符是一個順序點。

分支和跳轉

  • C99標準要求編譯器最少支持127層else-if嵌套。
  • C99標準爲邏輯運算符增加了可供選擇的拼寫方法。它們在iso646.h頭文件中定義,包含這個頭文件可以使用and,or,not來代替相應的邏輯運算符。同時,C還提供了3元字符擴展。
  • switch語句裏的case必須是整型(包括char)常量或者整型常量表達式(僅包含整數常量的表達式)。

函數

  • C99標準不再支持函數的int類型的默認設置。類型聲明是函數定義的一部分。
  • 一般來講,尾遞歸的空間複雜度是常量。
  • 所有的C函數地位同等,也就是說,main函數也可以被其本身或者被其他函數遞歸掉調用——儘管很少這麼做。

數組

  • 數組制定初始化項目,對數組中指定的項目初始化。如多次對一個元素初始化,則最後一次有效。
// 傳統語法
int arr1[6] = {0, 0, 0, 0, 0, 123};
// C99語法
int arr2[6] = {[5] = 123};
int arr3[3] = {[2] = 3, 4}; // 數組內容爲0, 3, 4
  • C99之前聲明數組的方括號內只能使用整數常量表達式(表達式的值必須大於0)。注意,與C++不同,const值不是一個整數常量。
  • C99引入了變長數組,即聲明數組的方括號內可以使用變量。變長數組必須是自動存儲類型的,這意味着他們必須在函數內部或者作爲函數形式參數聲明,而且聲明時不可以進行初始化。
int sum2d(int, int, int ar[*][*]);      // ar是一個邊長數組,必須使用*號代替省略的維數
int sum2d(int a, int b, int ar[a][b]);  // a和b的聲明一定要在ar的前邊
int sum2d(int ar[a][b], int a, int b);  // 錯誤
  • C99複合文字。可以使用複合文字創建一個無名數組。
// 一個包含兩個int值的數組
(int [2]){10, 20}
// 可以通過指針來使用他們,也可以將他們用做函數參數
int *p = (int [3]){1, 2, 3};

指針

  • 可以使用const來創建指向常量的指針或指針常量,當然也可以創建指向常量的指針常量。如下:
const int *pi1;     // 指向常量的指針,指針指向的值不能改變
int * const pi2;    // 指針常量,指針指向的地址不能改變,但是指針指向地址的值可以改變
  • 指向多爲數組的指針。
int (*pz)[2];       // pz指向一個包含兩個int值的數組
int *p[2];          // p是一個數組,這個數組的每一個元素是一個指向一個int值的指針
  • 指針兼容性。多維數組指針要求指針的維數是一致的。
int *pt;
int (*pa)[3];
int ar1[2][3];
int ar2[3][2];
int **p2;       // 指向指針的指針
pt = &ar1[0][0];
pt = ar1[0];
pt = ar1;       // 非法
pa = ar1;
pa = ar2;       // 非法
p2 = &pt;
*p2 = ar2[0];
p2 = ar2;       // 非法
  • const指針與非const指針。可以把一個非const指針賦值給一個const指針,但是不能把一個const指針賦值給一個非const指針。這個結論有一個前提,值進行一層間接運算。在兩層間接運算時,這樣的賦值就不再安全。
const int **pp2;
int *p1;
const int n = 13;
pp2 = &p1;      // 不允許,但我們假設允許
*pp2 = &n;      // 合法,二者都是const,這同時會使p1指向n
*p1 = 10;       // 合法,但這會改變const n的值
  • void指針。在C中void指針可以賦給其他類型的指針而不需要強制轉換。但在C++中需要。
  • 在C99中同樣可以聲明一個變長數組指針。

字符串

  • 字符串屬於靜態存儲類。
  • ANSI C提供了函數:atoi, atof, atol, strtol, strtoul, strtod來將字符串轉換成數值。
  • 可以使用sprintf來將數值轉換成字符串。

作用域

  • 一個C變量的作用域可以是代碼塊作用域,函數原型作用域或者文件作用域。
  • 一個C變量有靜態存儲時期和自動存儲時期。除非你顯示的初始化自動變量,否則他們不會被自動初始化。
  • 如果一個變量被聲明爲寄存器變量,你無法獲得它的地址。
  • 一個外部變量只能進行一次初始化,而且一定是在變量被定義時進行。

類型限定符

  • 限定詞volatile告訴編譯器該變量除了可被程序修改外還可以被其他代理改變。因此,這告訴編譯器要小心的優化這個變量。
  • 限定詞restrict只可以用於指針,表明該指針是訪問一個數據對象的唯一且初始的方式。這樣編譯器就可以進行合適的優化。一個例子是memcpy的參數使用了restrict,要求兩個內存區域不能有重疊,而memmove則不做這個假定。

文件I/O

  • stdout和stderr的一個區別是,當出現輸出重定向的時候,stderr仍然將輸出打印到屏幕上。即stderr不受重定向的影響。
  • 在Unix和Linux這樣只有一種文件類型的系統,打開文件時使用帶b字母的模式和不帶b字母的模式是相同的。
  • 程序中可以同時打開的文件數目是有限制的,這取決於系統和實現,通常爲10到20之間。

結構,聯合和枚舉

  • 指定項目初始化。與數組的類似。
struct person p = {.name = "gwq"}; // 僅僅指定名字
  • 在一些系統上,結構的佔用空間大小可能會大於他內部各個成員大小之和,這是因爲系統對數據的對齊存儲要求所致。
  • 和數組名不同,單獨的結構名不是該結構地址的同義詞。
  • 可以允許一個結構賦值給另一個結構,但是對數組不能這麼做。
  • C99複合文字和結構。
// 無名結構對結構賦值
struct person p = (struct person){"gwq", 21};
// 如果需要一個結構的地址,可以使用&獲得一個複合結構的地址。
struct person *pp = &(struct person){"gwq", 21};
  • C99的伸縮型數組成員。聲明一個伸縮型數組成員的規則是:1)伸縮性數組成員必須是最後一個數組成員。2)結構中必須至少有一個其他成員。3)伸縮型數組就像普通數組一樣被聲明,除了它的方括號內是空的。該數組成員的特殊屬性之一是它不存在,至少不立即存在。C99的意圖是使用malloc來分配足夠的空間來使用這個數組成員。如:
struct flex {
    int count;
    double average;
    double scores[];   // 伸縮型數組成員
};
// 現在這個pf指向的結構,擁有了5個元素的double類型數組。
struct flex *pf = malloc(sizeof(struct flex) + 5 * sizeof(double));
  • 聯合是一個能在同一個存儲空間裏(但不同時)存儲不同類型數據的數據類型。
  • 枚舉可以聲明代表整數常量的符號名稱。實際上,枚舉常量是int類型的。雖然枚舉常量是int類型的,但是枚舉變量較爲寬鬆的限定爲任一種整數類型,只要該整數類型能保存這些枚舉常量。
  • C的某些枚舉屬性不能延伸到C++中,如C允許對枚舉變量使用++,但C++不允許。
  • 枚舉的默認值是從零開始遞增。但可以制定特定的值。
enum color {red, green, blue};// 分別爲0, 1, 2
enum levels { low, medium = 500; high};     分別爲0500501
  • C使用術語名字空間(namespace)來表示識別一個名字的程序的部分。作用域是這個概念的一部分:名字相同但具有不同作用域的兩個變量不會衝突;而名字相同並在相同作用域中的兩個變量就會衝突。名字空間是分類別的。在一個特定作用域內的結構標記,聯合標記以及枚舉標記都共享一個名字空間,並且這個名字空間與普通變量使用的名字空間是不同的。如:
struct rect {double x; double y};
int rect;       // 在C中不會引起衝突

但是這種方式會引起混亂;而且,C++不允許在同一個作用域內對一個變量和一個標記使用同一個名字,因爲它把標記和變量名放在同一個名字空間中。

奇特的聲明

如下:

int board[8][8];        // int數組的數組
int **ptr;              // 指向int指針的指針
int *risks[10];         // 具有10個元素的數組,每個元素是一個指向int的指針
int (*rusks)[10];       // 一個指針,指向具有10個元素的int數組
int *oof[3][4];         // 一個3*4的數組,每個元素是一個指向int的指針
int (*uuf)[3][4];       // 一個指針,指向3*4int數組
int (*uof[3])[4];       // 一個具有3個元素的數組,每一個元素是指向具
                        // 有四個元素的int數組的指針

弄清楚這些聲明的訣竅便是理解使用修飾符的順序。

  • 表示一個數組的[]和表示一個函數的()具有相同的優先級,這個優先級高於間接運算符*的優先級。如:int *arr[10]聲明一個指針數組,而不是一個指向數組的指針。
  • []和()都是從左到右結合的。聲明int goods[10][50]使得goods是一個由12個具有50個int值的數組構成的數組,而不是一個由50個具有12個int值的數組構成的數組。
  • []和()具有相同的優先級,但是由於他們是從左到右結合的,所以聲明int (*rusks)[10]在應用方括號之前先將*和rusks組合在一起。這意味着rusks是一個指向具有10個int值的數組的指針。

函數和指針

  • 聲明一個指向特定函數類型的指針,首先聲明一個該類型的函數,然後用(*pf)形式的表達式代替函數名稱;pf就成爲了指向那種類型函數的指針了。
  • 使用指針調用函數有兩種看起來都合理的方式。如:
void ToUpper(char *);
void ToLower(char *);
void (*pf)(char *);
char mis[] = "Nina Metier";
pf = ToUpper;
(*pf)(mis);     // 把ToUpper作用於mis
pf = ToLower;
pf(mis);        // 把ToLower作用於mis

K&R C不允許第二種形式,但是有的實現卻採用第二種形式,爲了保持與現有代碼的兼容性,ANSI C把這二者作用等價形式全部接受。

  • 不能擁有一個函數的數組,但是可以擁有一個函數指針的數組。如char (*pf[3])(void);

位操作

  • 掩碼。使用位與(&)可以獲得特定某個位或某些位的值。
  • 打開位。使用位或(|)可以將某一位置爲1而不管這個位原來是多少。
  • 關閉位。使用位與(&)與求反(~)可以將某個位置爲0,不管這個位原來是多少。如:
int mask = 1;
int n = 3;
int m = n & ~mask;     // 將最低位關閉
  • 轉置位,因爲1與0異或爲1,1與1異或爲0。而0與0異或爲0,0與1異或爲1。所以可以使用異或操作(^)來轉置某些位。只需要將需要轉置的位的掩碼置爲1,其餘置爲0就行了。
  • 左移位運算符(<<)將其左側操作數的值的每位向左移動,移動的位數由其右側操作數指定。空出的位用0填充,並且丟棄移出左側操作數末端的位。
  • 右移位運算符(>>)將其左側操作數的值的每位向右移動,移動的位數由其右側操作數指定。丟棄移出左側操作數右端的位。對於unsigned類型,使用0填充左端空出的位。對於有符號數,結果依賴機器。空出的位可能用0填充,或者使用符號(最左端的)位的副本填充。
  • 移位運算符能夠提供快捷,高效的(依賴於硬件)的對2的冪的乘法和除法。
  • 可以在結構中使用位字段,即對其的操作僅僅是對這個或這幾個位的操作。使用一個寬度爲0的未命名字段迫使下一個字段與下一個整數對齊。不允許一個字段跨越兩個unsigned int之間的邊界,編譯器自動的移位這樣一個字段定義,使得字段按unsigned int邊界對齊。發生這種情況時,會在第一個unsigned int中留下一個未命名的洞。如:
struct {
    unsigned int field1: 1;
    unsigned int       : 2;
    unsigned int field2: 1;
    unsigned int       : 0;
    unsigned int field3: 1;
}stuff;

以上例子中,stuff.field1與stuff.field2之間有一個2位的間隙,stuff.field3存儲在下一個int中。
- 一個重要的機器依賴性是將字段放置到一個int中的順序。在有些機器上,這個順序是從左向右;在另一些機器上順序是從右向左。另外,不同機器在兩個字段間邊界的位置上也有區別。由於這些原因,位字段往往難以移植。
- 同樣也可使用位運算來模仿位字段的功能,但這要稍微複雜一些。

C預處理器和庫

  • C的預處理只進行字符替換,不會進行算數運算。如:
    #define TWO 2
    int x = TWO * TWO;    // 進過預處理後是2 * 2,實際想乘發生在編譯階段
  • 預處理器不會替換在字符串中的宏。如:
#define TWO 2
#define OW "nihao"
printf("TWO:OW");// 打印出來TWO:OW而不是2:nihao
  • 對於重定義宏,ANSI C只允許新定義與舊定義完全相同。相同定義意味着主體具有相同順序的語言符號。
  • 在宏定義中可以使用參數。但這使用不當可能會帶來難以理解的結果。因爲宏只進行簡單的文本替換,可能一個參數在宏中會出現好多次,這對有副作用的表達式,會產生多次副作用,典型的就是自增和自減運算符。
  • 可以使用#號將一個宏的參數字符串化。如:
#define PR(x) printf(#x " = %d.\n", x);
int x = 56;
PR(x);        // 替換爲:printf("x" " = %d.\n", x);
  • 可以使用##符號來將一個語言符號組合成一個語言符號,可以理解爲生成一個變量的名稱。如:
XNAME(n) x ## n
int XNAME(1) = 4;       // 替換爲int x1 = 4;
  • 可以使用…和__VA_ARGS__來定義一個可變參數的宏。方法是將宏定義中參數列表的最後一個參數寫爲省略號,然後在被替換部分就可以使用__VA_ARGS__來代替省略的部分。如:
#define PR(...) printf(__VA_ARGS__)
PR("nihao");    // printf("nihao");
PR("%d", x);    // printf("%d", x);
  • 函數調用需要一定的開銷,這意味着執行調用時花費了時間用於建立調用,傳遞參數,跳轉到函數代碼段並返回。使用類函數宏的一個原因就是可以減少執行時間。此外C99還提供了另外一種方法:內斂函數。C99標準這樣敘述:“把函數變爲內聯函數將建議編譯器儘可能快速地調用該函數。上述建議的效果由實現來定義”。因此,使函數變爲內聯函數可能會簡化函數的調用機制,但也可能不起作用。
  • 因爲內聯函數沒有預留給它單獨代碼塊,所以無法獲得內斂函數的地址(實際上,可以獲得地址,但這樣會使編譯器產生非內聯代碼)。另外,內聯函數不會在調試器中顯示。
  • 內聯函數應該比較短小。對於很長的函數,調用函數的時間少於執行函數主體的時間;此時,使用內斂函數不會節省很多時間。
  • 編譯器在優化內聯函數時,必須知道函數定義的內容,這意味着內聯函數的定義和對該函數的調用必須在同一文件中,正因爲這樣,內聯函數通常具有內部連接。在多文件的程序中,每個調用內聯函數的文件,都要對該函數進行定義。
  • main函數在結束時會隱式的調用exit();可以使用atexit函數註冊在程序(隱式或顯示)調用exit函數前,調用的函數。可以註冊多個函數,最後註冊的函數最先被調用。ANSI保證這個列表中至少可以放置32個函數。

注:以上這些文字,大部分抄錄自C Primer Plus(第五版)中文版。

發佈了124 篇原創文章 · 獲贊 15 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章