讀了一遍著名的《the C programming language》,果然如聽說的一樣,講解基礎透徹,案例簡單典型,確實自己C語言還有很多細節點不是很清楚。
總結一下閱讀的收獲(部分原書不清晰的知識點在網絡上搜索後補充,引用出處忘記了,原作者看到可聯系添加)
1.聲明
1.1 變量聲明
在C語言中,所有變量都必須先說明後使用,說明通常放在函數開始處的可執行語句之前。
1.2 外部變量
在每一個函數中都要對所要訪問的外部變量進行聲明。聲明所使用的外部變量的類型,在聲明時可以用extern顯式說明,也可以通過上下文隱式說明。如果外部變量的定義在源文件中出現在使用它的函數之前,則extern聲明可以省略。
如果程序包含幾個源文件,某個變量在file1中定義,在file2與file3中使用,那麼file2和file3文件中就需要extern聲明來連接該變量的出現。
變量extern聲明和函數聲明放在頭文件中。
1.3 聲明的風格
聲明的舊風格:print();(省去的返回值int,簡化編碼)
聲明的新風格:void print(void)
新風格的優點:編譯器幫助更好地檢查函數調用
如:
print(5.5);
在舊風格下,該次調用編譯器不會報錯(因為參數列表沒有內容意味著不對參數列表進行檢查)
在新風格下,編譯器會報錯
1.4 聲明的重要性(編譯器檢查之反例)
在同一源文件中,函數的聲明必須與其定義一致,否則編譯錯誤。但如果函數是獨立編譯的,則這種不匹配就不會檢測出來。因為C語言會對沒有函數聲明的函數自動進行 隱式聲明,那麼編譯器不僅不會對返回值進行檢測,更不會對參數列表進行檢測。
main.c
printf(“%f”,getNum( }
get_num.c
get_num( }
編譯不會有任何錯誤,務必記得使用函數聲明
在缺省的情況下,外部變量與函數具有如下性質:所有通過名字對外部變量與函數的引用(即使這種引用來自獨立編譯的函數)都是引用的同一對象。
2. 內存布局
2.1 int的字節長度
在C語言中int的長度
1. long int型至少應該與int型一樣長,而int型至少與short int型一樣長;
2. C/C++規定int的字長與機器字長相同
3. 操作系統字長與機器字長未必一致
4. 編譯器根據操作系統字長來定義int字長
由上面4點可知,在一些沒有操作系統的嵌入式計算機系統上,int的字長與處理器字長相同;有操作系統時,操作系統字長與處理器字長不一定一致,此時編譯器根據操作系統的字長來定義int字長。比如你在64位機器上運行DOS的16位系統,那麼所有for dos的C/C++編譯器中的int都是16位的;在64位機器上運行win32系統,那麼所有for win32的C/C++編譯器中的int都是32位的。
原因:
操作系統決定了軟件層面對於硬件的管理方式,那麼對於64位的機器,如果管理方式仍然是16位的(如內存訪問地址僅為216),那麼該64位機器其實也只發揮了16位的作用。
對於不同的平台,有其相應的指令集,也就有其對應的編譯器。C語言在源代碼層面具有可移植性就是這個原因,只要在不同的平台,使用不同的編譯器,即使最終得到的二進制機器碼不同,程序運行結果也一定是相同的。
因此,定義數據結構時(尤其是嵌入式)時,優秀的程序員不會如下定義(32位):
typedef unsigned unsigned }TypeExample;
他們這樣定義:
unsigned short UINT16 unsigned int UINT32 typedef }TypeExample;
根據IEEE754的標准,float和double的內存布局如下:
符號位S(1 bit) + 指數(8 bits) + 尾數(23 bits)
float長度32位
計算方式:
指數上為移碼,偏移數為127
尾數上省去了最高位的1,僅作為小數點後的二進制表示
(-1)^S(1+尾數)*2^(指數-偏移數)
例:3.0 = 11 = (-1)^0 * (1.1) * 2^(128 -127)
故:符號位為0,指數為1298,尾數為:1+22個0
所以:3.0的float為:0 10000000 10000000000000000000000 = 0x40400000
驗證:
#include <stdio.h> t.b = printf( }
結果輸出為:3.000000
因此,指數127~128,范圍為:2^-127~2^128
尾數:隱藏的1永遠不變,不會影響精度。2^23 = 8388608,一共七位,這意味著最多能有7位有效數字,但絕對能保證的為6位,也即float的精度為6~7位有效數字.
同樣,對於內存布局為:
符號位S(1 bit) + 指數(11 bits) + 尾數(52 bits)
的double類型來說,分析是相同的。
范圍:-2^1023 ~ +2^1024
有效數字:2^52 = 4503599627370496,一共16位,同理,double的精度為15~16位。
3. 類型
3.1 char變量的可移植性
定義變量時,只是用關鍵字char,缺省情況下,根據編譯器定義為signed或者unsigned,這樣會導致不同機器上char有不同的取值范圍。
若顯式地將字符聲明為signed或者unsigned,則可提高平台可移植性,但機器處理signed和unsigned的能力不同,會導致效率受損。還有不同處理字符的庫函數的參數聲明為char,顯式聲明會帶來兼容性問題。
結論:保證可移植性的最佳方法還是定義為char型同時只是用signed char和unsigned char的交集字符,在進行算術運算時,顯式使用。
3.2 運算分量在運算前完成提升
如果某個算數運算符有一個浮點分量和一個整數運算分量,那麼合格整數運算分量在開始運算之前會被轉換為浮點類型
表達式先進行類型轉換,再計算 z = ( n > 0) ? f : n
無論n是否為正,z的類型都是float
3.3 浮點常量
浮點常量攜程帶小數點。如:3.0
3.4 使用unsigned char類型來接受ASCII碼的問題
unsigned ( (c=getchar()) != }
EOF宏定義的值為-1,而unsigned char 無法接受-1,所以永遠無法到達文件結尾
3.5 常量表示
<limits.h>與<float.h>包含了所有這些類型的符號常量以及機器與編譯程序的其他性質
long常量要以字母L或l結尾
無符號數以u或U結尾
後綴ul或UL用於表示unsigned long常量
浮點常量的表示方式:123.4或1e-2,無後綴為 double,後綴f或F為float,後綴l或L為long double
3.6 進制表示
十進制 31 八進制 037 十六進制 0x1F 二進制 0b00011111
3.7位模
使用位模式來指定字符對應的ASCII碼
ASCII碼縱向制表符
11 ‘\v’
= ‘\xb’ (‘\xhh’ hh為1至多個十六位進制數)
= ‘\013’(‘\ooo’為1至3個八進制數)
3.8 字符串表示
C語言對字符串長度無限制,但程序必須掃描完整的字符串(’\0’結束符)才能決定這個字符串的長度
strlen 返回字符串長度,不包括結束符
3.9 枚舉類型
枚舉常量
1.enum boolean { NO,YES };
枚舉值從0開始遞增 NO = 0 YES = 1
2.enum month { JAN = 1, FEB , MAR , APR };
JAN =1 FEB = 2 MAR = 3 APR = 4
3.enum escapes { BELL = ‘\a’ , BACKSPACE = ‘\b’ , TAB = ‘\t’ , NEWLINE = ‘\n’ };
顯示指定枚舉值
盡量用const、enum、inline替換#define,寧可以編譯器替換預處理器(EFFECTIVE C++)
宏在預處理階段進行替換工作,它替換代碼段的文本,程序運行的時候,宏已經不存在了。而枚舉是在程序運行之後起作用的,枚舉常量存儲在數據段的靜態存儲區中,宏占用代碼段的空間,而枚舉除了占用空間,還消耗CPU資源。
枚舉類型值能自動生成,這是相對於#define的優勢
3.10 自動變量
只在函數內部定義使用的變量。它只是允許在定義它的函數內部使用它,在函數外的其他任何地方都不能使用的變量。系統自動完成對自動變量存儲空間的分配和回收,它的生命周期是從它們被定義到定義它們的函數返回。這個過程是通過一個堆棧機制來實現的,為自動變量分配內存就是壓棧,返回就是退棧。
3.11 靜態變量
不像自動變量使用堆棧機制使用內存,而是在靜態存儲區分配固定的內存。持續性是程序運行的整個周期。作用域為定義它的函數的內部。
通過extern訪問其他文件中定義的全局變量,如果使用static在函數外面聲明變量,則其他文件不允許使用該變量。const int a聲明在函數外也只能在定義它的文件中使用。
3.12 寄存器變量
提高訪問效率,具體是否使用寄存器由編譯器決定,其地址不能被訪問。
register ra asm(“ebx”);
3.13 易失變量
強制訪問操作,防止編譯器在優化,告訴編譯器從內存中取值,而不是從寄存器或緩存。
3.14 非自動變量
非自動變量包括:全局變量+靜態變量
非自動變量只初始化一次,在程序開始之前進行,且初始化符為常量表達式,其缺省為0
自動變量進行其所在函數即初始化,其初始化符可以是任意表達式,未經初始化值為未定義
4.類型轉換
4.1 int到char的轉換
實質上僅僅是一個ASCII碼表的映射關系的轉換。
C語言內部內置了這種映射關系, 使用char類型管理字符,其實是在管理ASCII碼的值,最終輸出時完成到字符的映射就行了。
將int賦值給char時,實質上做的是內存截斷
例:
a = c = pritnf(“%c”,c);
結果為a
a = c = printf(“%c”,c);
結果同為a
ASCII碼表 0~255(0~127標准ASCII碼 128~255 擴展ASCII碼)
4.2 char到int的轉換
C語言未指定char類型是有符號還有無符號,所以把char類型的值轉換為int類型的值時,視機器不同而有所變化。
某些機器最左邊為1,那麼就被轉換為負整數,而另一些則提升,在最左邊添加0
代碼:
a = b = printf(“% }
強制類型轉換的精確定義:
表達式首先被賦給類型名指定類型的某個變量(會自動構造對應的內存布局),然後再將其用在整個構造所在的位置。
參數是通過函數原型聲明,那麼通常情況下,當函數被調用時,系統對參數自動進行強制類型轉換,但是對於printf來說,%f等格式控制符僅僅是決定對對應參數的解釋方式,是不會進行強制類型轉換的。
運算前,對於運算分量先把“低”的類型提成為“高”的類型
4.4 float的自動轉換
注意:在表達式中的float類型的運算分量不自動轉換成double類型,這與原來的定義不同。一般而言,數學函數要用雙精度。使用float類型的主要原因是為了使用較 大的數組時節省存儲空間,有時也為了機器執行時間(雙精度運算特別費時)
4.5 unsigned類型的自動轉換
包含unsigned類型的運算分量時,轉換規則要復雜一些。主要問題是,有符號值與無符號值之間的比較取決於機器因為它們取決於各個整數類型的大小。
若int為16位,long為32位
則-1L<1U 因為unsigned int會轉化為signed long類型
-1L>1UL,因為-1L會被轉化為unsigned long類型
5.運算
賦值運算的結合次序:從右到左
5.2 位運算
例:使用bitcount函數統計變量中值為1 的位的個數。
方法1:每一位進行匹配移位,直到x為0
bitcount( unsigned ( b = ; x != ; x >>== ( x & b ++ }
方法2:去除變量最右端的1直到變量大小為0
bitcount( unsigned ( b = ; x != ; b++ x &= (x - }
5.3 表達式先進行類型轉換後進行運算
如計算:
z = ( n > ) ? f : n;
無論n是否為正,z的類型都是float
5.4 函數調用中變量的求值次序
在函數調用中各個變量的求值次序也是未指定的
printf(“%d %d \n”, ++n, power(,n));
錯誤,不同編譯程序會決定是否在power(2,n)之前對n執行++操作
故改寫為:
++%d %d \n”, n , power(,n));
5.5 加一、減一運算的副作用
a[i] = i++;
數組下標是舊值還是新值,編譯程序對之可以有不同的解釋,並視為不同的解釋,產生不同的結果。
5.6三元運算符的使用
三元運算符的使用,可以有效節省代碼長度,如:
( i =; i <; i++%d %s”,i,(i != ) ? " " : "\n";
6.語句
6.1 if , while , for的條件測試部分真的意思是”非0”
6.2 switch語句
switch語句中case情形的作用就像標號一樣,在某個case情形的代碼執行完後,就進入下一個case情形執行,除非顯示控制轉出。
6.3 for循環中可以使用逗號運算符”,”,支持多個表達式
6.4 變量和函數可以一起聲明
sum,atof( []);
7.其他
7.1(標准庫函數)printf
%d 十進制 %o 八進制 %x 十六進制 %f = %lf
加h短整型 加l長整型
printf中的寬度、精度可由*號來控制
例如:
printf(“%.*s”,max,s);
%後跟-符號表述左對齊,
如:
a = %-4d”,a);
定義如printf這樣的帶有變長參數表的函數時,參數表至少有一個參數
minprintf( *fmt,...);
7.2 定義與聲明的區別
定義:變量建立或分配存儲單元的位置
聲明:指明變量性質的位置,不分配存儲單元
7.3 變量的定義不是只能出現在函數開始的部分
7.4 數組初始化
數組初始化,沒有被初始化的部分自動置0
字符數組的初始化
pattern[] == pattern[] = “’o’,’u’,’l’,’d’,’\’”;
7.5 宏定義
7.5.1 宏定義中的加一、減一
max(A,B) = ((A) > (B) ? (A) : (B))
該宏對於傳入的i++情形,會加兩次
7.5.2 宏定義中的字符串
參數名以#為前綴,那麼它們將被由實際參數替換的參數擴展成帶引號的字符串
dprint(expr) printf(#expr “ = %g \n”, expr);
dprint(x//y”” = %g \n”, x/y);
輸出結果為:x/y = ?
7.5.3 ##為宏擴展提供了一種連接實際參數的手段
paste(front,back) front ## back) ;
7.5.4 #if語句中包含一個常量整數表達式(其中不得包含sizeof,強制類型轉換運算符或枚舉常量),在#if語句中可以使用一個特殊的表達式defined(名字)。
!defined(EDR) HDR .. ..
兩個特殊的表達式
#ifdef = defined(***)= !defined(***)
7.6 取地址運算符只能應用於內存對象,不能對表達式、常量或寄存器變量進行操作
7.7 const限定符
1. 用const修飾一般變量
const修飾的變量必須在聲明的時候進行初始化,一旦一個變量被const修飾後,在程序中除初始化外對這個變量進行的賦值都是錯誤的。
2. const與指針搭配使用
指針常量,即指針本身的值不可改變
常量指針,即指針指向的變量的值是不可以改變的
const int *p 和int const *p1;const形容的指針指向的內容
int * const p2 = &b; const形容的是指針本身
const修飾的變量需要初始化
3. 作為函數參數
4. 節省空間,避免了不必要的內存分配
const定義常量從匯編角度來看,只是給出了對應的內存地址,而不是像#define一樣給出了立即數,所以,const定義的常量在程序運行過程中只有一份拷貝,而#define定義的常量在內存中有若干個拷貝。
5. 編譯器通常不為普通const常量分配存儲空間,而是將他們保存在符號表中。這使得它成為一個編譯期間的常量。沒有了存儲與讀內存的操作,使得它的效率也很 高。
6. 阻止用戶修改函數返回值
7. 結構體中const成員變量的初始化
A s = {,};
與結構體的初始化相同
8. const是只讀變量
n= a[n];
錯誤。const是只讀變量,而非常量。
9. const變量 & const限定的內容
typedef * [] = *p1 = pStr p2 = ++; p2++;
分析:
1)const使用的基本形式:const char m 限定m不變
2)替換1式中的m,const char *pm;限定*pm不可變。當然pm是可變的,因此p1++是正確的。
3)替換1式char const newType m;限定m不可變,問題中的pStr是一種新類型,因此問題中的p2不可變。p2++是錯誤的。
10. 字符串常量與字符數組
char *c = “Hello World”;字符常量
char c[] = “Hello World”;字符數組
字符串常量存在靜態存儲區(只讀的,rodata段中)
字符數組放在動態存儲區
所以字符串常量不能被修改 c[1] = ‘a’