the C programming language 閱讀筆記1

讀了一遍著名的《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 

1 int main()
2 {
3     printf(「%f」,getNum(123));
4 }

  get_num.c

1 double get_num(void)
2 {
3     return 3.14;
4 }

  編譯不會有任何錯誤,務必記得使用函數聲明

  在缺省的狀況下,外部變量與函數具備以下性質:全部經過名字對外部變量與函數的引用(即便這種引用來自獨立編譯的函數)都是引用的同一對象。

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位):

1 typedef struct tagTypeExample{
2 
3 unsigned short x;
4 
5 unsigned int y;
6 
7 }TypeExample;

他們這樣定義:

 1 #define unsigned short UINT16
 2 
 3 #define unsigned int UINT32
 4 
 5 typedef struct tagTypeExample{
 6 
 7 UINT x;
 8 
 9 UINT y;
10 
11 }TypeExample;

這樣,換平臺的話,只須要改變宏定義便可

2.2 float和double的範圍和有效數字

  根據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

  驗證:

 1 #include <stdio.h>
 2 
 3 union test{
 4     float a;
 5     int b;
 6 };
 7 int main()
 8 {
 9     union test t;
10     t.b = 0x40400000;
11     printf("%f\n",t.a);
12 }

  結果輸出爲: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碼的問題

1 int main()
2 {
3     unsigned char c;
4     while( (c=getchar()) != EOF){
5         putchar(c);        
6     }
7     return 0;
8 }

  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 寄存器變量

  提升訪問效率,具體是否使用寄存器由編譯器決定,其地址不能被訪問。

1 register int a;
2 int ra asm(「ebx」);

3.13 易失變量

  強制訪問操做,防止編譯器在優化,告訴編譯器從內存中取值,而不是從寄存器或緩存。

3.14 非自動變量

  非自動變量包括:全局變量+靜態變量

  非自動變量只初始化一次,在程序開始以前進行,且初始化符爲常量表達式,其缺省爲0

  自動變量進行其所在函數即初始化,其初始化符能夠是任意表達式,未經初始化值爲未定義

 

4.類型轉換

4.1 int到char的轉換

  實質上僅僅是一個ASCII碼錶的映射關係的轉換。

  C語言內部內置了這種映射關係, 使用char類型管理字符,實際上是在管理ASCII碼的值,最終輸出時完成到字符的映射就好了。

  將int賦值給char時,實質上作的是內存截斷    

  例:

1 a = 0x62;
2 char c = a;
3 pritnf(「%c」,c);

  結果爲a 

1 a = 0xFF62;
2 char c = a;
3 printf(「%c」,c);

  結果同爲a

  ASCII碼錶 0~255(0~127標準ASCII碼  128~255 擴展ASCII碼)

4.2 char到int的轉換

  C語言未指定char類型是有符號還有無符號,因此把char類型的值轉換爲int類型的值時,視機器不一樣而有所變化。

  某些機器最左邊爲1,那麼就被轉換爲負整數,而另外一些則提高,在最左邊添加0

  代碼:

1 int main()
2 {
3 char a =0xFF;
4 int b = a;
5 printf(「%d」,b);
6 return 0;
7 }

  本機測試結果爲-1,被轉換爲了0xFFFFFFFF,被轉換爲了負整數

4.3 強制類型轉換

  強制類型轉換的精肯定義:

  表達式首先被賦給類型名指定類型的某個變量(會自動構造對應的內存佈局),而後再將其用在整個構造所在的位置。

  參數是經過函數原型聲明,那麼一般狀況下,當函數被調用時,系統對參數自動進行強制類型轉換,可是對於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.1 賦值運算

  賦值運算的結合次序:從右到左

5.2 位運算

  例:使用bitcount函數統計變量中值爲1 的位的個數。

  方法1:每一位進行匹配移位,直到x爲0

1 int bitcount( unsigned int x)
2 {
3     int b;
4     for( b = 0; x != 0; x >>== 1)
5         if( x & 01)
6             b ++;
7     return b;
8 }

  方法2:去除變量最右端的1直到變量大小爲0

1 int bitcount( unsigned int x )
2 {
3     for( b = 0; x != 0; b++ )
4         x &= (x -1);
5     return b;
6 }

5.3 表達式先進行類型轉換後進行運算

  如計算:

z = ( n > 0) ? f : n;

  不管n是否爲正,z的類型都是float

5.4 函數調用中變量的求值次序

  在函數調用中各個變量的求值次序也是未指定的

printf(「%d %d \n」, ++n, power(2,n));

  錯誤,不一樣編譯程序會決定是否在power(2,n)以前對n執行++操做

  故改寫爲:

++n;
printf(「%d %d \n」, n , power(2,n));

5.5 加1、減一運算的反作用

a[i] = i++;

  數組下標是舊值仍是新值,編譯程序對之能夠有不一樣的解釋,並視爲不一樣的解釋,產生不一樣的結果。

5.6三元運算符的使用

  三元運算符的使用,能夠有效節省代碼長度,如: 

for(int i =0; i <5; i++)
    pritnf(「%d %s」,i,(i != 4) ? " " : "\n";

 

6.語句

6.1 if , while , for的條件測試部分真的意思是」非0」

6.2 switch語句

  switch語句中case情形的做用就像標號同樣,在某個case情形的代碼執行完後,就進入下一個case情形執行,除非顯示控制轉出。

6.3 for循環中可使用逗號運算符」,」,支持多個表達式

6.4 變量和函數能夠一塊兒聲明

double sum,atof(char []);

 

7.其餘 

7.1(標準庫函數)printf 

  %d 十進制 %o 八進制 %x 十六進制 %f = %lf

  加h短整型 加l長整型

  printf中的寬度、精度可由*號來控制

  例如: 

printf(「%.*s」,max,s);

  %後跟-符號表述左對齊,

  如:

int a = 1;
printf(「%-4d」,a);//輸出在最左,沒法展現出4位字寬

  定義如printf這樣的帶有變長參數表的函數時,參數表至少有一個參數

void minprintf( char *fmt,...);

7.2 定義與聲明的區別

  定義:變量創建或分配存儲單元的位置

  聲明:指明變量性質的位置,不分配存儲單元

7.3 變量的定義不是隻能出如今函數開始的部分

7.4 數組初始化

  數組初始化,沒有被初始化的部分自動置0

  字符數組的初始化   

char pattern[] = 「ould」;

= char pattern[] = {’o’,’u’,’l’,’d’,’\0’};

7.5 宏定義

  7.5.1 宏定義中的加1、減一

#define max(A,B) = ((A) > (B) ? (A) : (B))

  該宏對於傳入的i++情形,會加兩次

  7.5.2 宏定義中的字符串

  參數名以#爲前綴,那麼它們將被由實際參數替換的參數擴展成帶引號的字符串

#define dprint(expr) printf(#expr 「 = %g \n」, expr);
dprint(x/y);
printf(「x/y」」 = %g \n」, x/y);

  輸出結果爲:x/y = ?

  7.5.3 ##爲宏擴展提供了一種鏈接實際參數的手段

#define paste(front,back) front ## back
paste(name,1) ; //獲得name1

  7.5.4 #if語句中包含一個常量整數表達式(其中不得包含sizeof,強制類型轉換運算符或枚舉常量),在#if語句中可使用一個特殊的表達式defined(名字)。

#if !defined(EDR)
    #define HDR
#endif

#if ..
..
#elif ..
..
#else
..
#endif

  兩個特殊的表達式

#ifdef  = #if defined(***)
#ifndef = #if !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成員變量的初始化

struct A s = {10,2};

  與結構體的初始化相同

  8.  const是隻讀變量

const int n= 5
int a[n];

  錯誤。const是隻讀變量,而很是量。

  9.      const變量 & const限定的內容

typedef char * pStr;
char string[4] = 「abc」;
const char *p1 = string;
const pStr p2 = string;
p1++;//正確
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’

相關文章
相關標籤/搜索