1. 首頁
  2. 讀書筆記

c語言讀書筆記

c語言讀書筆記

C語言是一門通用計算機程式語言,應用廣泛。C語言的設計目標是提供一種能以簡易的方式編譯、處理低階儲存器、產生少量的機器碼以及不需要任何執行環境支援便能執行的程式語言。品才網整理了C語言的讀書筆記,歡迎大家閱讀。

c語言讀書筆記

《C 語言深度解剖》這本書是一本“解開程式設計師面試筆試的秘密”的好書。作者陳正衝老師提出“以含金量勇敢挑戰國內外同類書籍”,確實,這本書中的知識點都是一些在面試中常見的考點,並且很多都是我們平常不注意的點,對於我們深入理解C語言確實很有幫助。

第1章關鍵字

1.register

雖然暫存器的速度非常快,但是使用register修飾符也有些限制的:register變數必須是能被CPU暫存器所接受的型別。

意味著register變數必須是一個單個的值,並且其長度應小於或等於整型的長度。而且register變數可能不存放在記憶體中,

所以不能用取址運算子“&”來獲取register變數的地址。

2.static修飾符

(1)修飾變數

靜態區域性變數,在函式體裡面定義的,就只能在這個函數里用了,同一個文件中的其他函式也用不了。由於被static修飾的變數總是存在記憶體的靜態區,所以即使這個函式執行結束,這個靜態變數的值還是不會被銷燬,函式下次使用時仍然能用到這個值。

(2)修飾函式

第二個作用:修飾函式。函式前加static使得函式成為靜態函式。但此處“static”的含義不是指儲存方式,而是指對函式的作用域僅侷限於本檔案(所以又稱內部函式)。使用內部函式的好處是:不同的人編寫不同的函式時,不用擔心自己定義的函式,是否會與其它檔案中的函式同名。

關鍵字static有著不尋常的歷史。起初,在C中引入關鍵字static是為了表示退出一個塊後仍然存在的區域性變數。隨後,static在C中有了第二種含義:用來表示不能被其它檔案訪問的全域性變數和函式。為了避免引入新的關鍵字,所以仍使用static關鍵字來表示這第二種含義。

3.if語句使用注意

先處理正常情況,再處理異常情況。

在編寫程式碼是,要使得正常情況的執行程式碼清晰,確認那些不常發生的異常情況處理程式碼不會遮掩正常的執行路徑。這樣對於程式碼的可讀性和效能都很重要。因為,if

語句總是需要做判斷,而正常情況一般比異常情況發生的機率更大(否則就應該把異常正常調過來了),如果把執行機率更大的程式碼放到後面,也就意味著if語句將進行多次無謂的比較。

另外,非常重要的一點是,把正常情況的處理放在if後面,而不要放在else後面。當然這也符合把正常情況的處理放在前面的要求。

4.千萬小心又小心使用void指標型別。

按照ANSI(AmericanNationalStandardsInstitute)標準,不能對void指標進行演算法操作,即下列操作都是不合法的:

void*pvoid;

pvoid++;//ANSI:錯誤

pvoid+=1;//ANSI:錯誤

ANSI標準之所以這樣認定,是因為它堅持:進行演算法操作的指標必須是確定知道其指向資料型別大小的。也就是說必須知道記憶體目的地址的確切值。

例如:

int*pint;

pint++;//ANSI:正確

但是大名鼎鼎的GNU(GNU'sNotUnix的遞迴縮寫)則不這麼認定,它指定void*的演算法操作與char*一致。因此下列語句在GNU編譯器中皆正確:

pvoid++;//GNU:正確

pvoid+=1;//GNU:正確

在實際的程式設計中,為符合ANSI標準,並提高程式的可移植性,我們可以這樣編寫實現同樣功能的程式碼:

void*pvoid;

(char*)pvoid++;//ANSI:正確;GNU:正確

(char*)pvoid+=1;//ANSI:錯誤;GNU:正確

GNU和ANSI還有一些區別,總體而言,GNU較ANSI更“開放”,提供了對更多語法的支援。但是我們在真實設計時,還是應該儘可能地符合ANSI標準。

5.const與宏

節省空間,避免不必要的記憶體分配,同時提高效率

編譯器通常不為普通const只讀變數分配儲存空間,而是將它們儲存在符號表中,這使得它成為一個編譯期間的值,沒有了儲存與讀記憶體的操作,使得它的效率也很高。

例如:

#define M 3//宏常量

const int N=5;//此時並未將N放入記憶體中

......

inti=N;//此時為N分配記憶體,以後不再分配!

intI=M;//預編譯期間進行宏替換,分配記憶體

intj=N;//沒有記憶體分配

intJ=M;//再進行宏替換,又一次分配記憶體!

const定義的只讀變數從彙編的角度來看,只是給出了對應的記憶體地址,而不是象#define一樣給出的是立即數,所以,const定義的只讀變數在程式執行過程中只有一份複製(因為它是全域性的只讀變數,存放在靜態區),而#define定義的宏常量在記憶體中有若干個複製。#define宏是在預編譯階段進行替換,而const修飾的只讀變數是在編譯的時候確定其值。

#define宏沒有型別,而const修飾的只讀變數具有特定的型別。

6.最易變的關鍵字----volatile

volatile是易變的、不穩定的意思。很多人根本就沒見過這個關鍵字,不知道它的存在。也有很多程式設計師知道它的存在,但從來沒用過它。我對它有種“楊家有女初長成,養在深閨人未識”的感覺。volatile關鍵字和const一樣是一種型別修飾符,用它修飾的變量表示可以被某些編譯器未知的因素更改,比如作業系統、硬體或者其它執行緒等。遇到這個關鍵字宣告的變數,編譯器對訪問該變數的程式碼就不再進行最佳化,從而可以提供對特殊地址的穩定訪問。

先看看下面的例子:

int i=10;

int j=i;//(1)語句

int k=i;//(2)語句

這時候編譯器對程式碼進行最佳化,因為在(1)(2)兩條語句中,i沒有被用作左值。這時候編譯器認為i的值沒有發生改變,所以在(1)語句時從記憶體中取出i的值賦給j

之後,這個值並沒有被丟掉,而是在(2)語句時繼續用這個值給k賦值。編譯器不會生成出彙編程式碼重新從記憶體裡取i的值,這樣提高了效率。但要注意:(1)(2)語句之間i沒有被用作左值才行。

再看另一個例子:

volatile int i=10;

int j=i;//(3)語句

int k=i;//(4)語句

volatile關鍵字告訴編譯器i是隨時可能發生變化的,每次使用它的時候必須從記憶體中取出i的值,因而編譯器生成的彙編程式碼會重新從i的地址處讀取資料放在k中。

這樣看來,如果i是一個暫存器變數或者表示一個埠資料或者是多個執行緒的共享資料,就容易出錯,所以說volatile可以保證對特殊地址的穩定訪問。

但是注意:在VC++6.0中,一般Debug模式沒有進行程式碼最佳化,所以這個關鍵字的作用有可能看不出來。你可以同時生成Debug版和Release版的程式做個測試。

留一個問題:const volatile int i=10;這行程式碼有沒有問題?如果沒有,那i到底是什麼屬性?

這個可以同時使用。

7.空結構體是有大小的

structstudent

{

}stu;

sizeof(stu)的值是多少呢?在VisualC++6.0上測試一下。

很遺憾,不是0,而是1。為什麼呢?你想想,如果我們把structstudent看成一個模子的話,你能造出一個沒有任何容積的模子嗎?顯然不行。編譯器也是如此認為。編譯器認為任何一種資料型別都有其大小,用它來定義一個變數能夠分配確定大小的空間。既然如此,編譯器就理所當然的認為任何一個結構體都是有大小的,哪怕這個結構體為空。那萬一結構體真的為空,它的大小為什麼值比較合適呢?假設結構體內只有一個char型的資料成員,那其大小為1byte(這裡先不考慮記憶體對齊的情況).也就是說非空結構體型別資料最少需要佔一個位元組的空間,而空結構體型別資料總不能比最小的非空結構體型別資料所佔的空間大吧。這就麻煩了,空結構體的大小既不能為0,也不能大於1,怎麼辦?定義為0.5個byte?但是記憶體地址的最小單位是1個byte,0.5個byte怎麼處理?解決這個問題的最好辦法就是折中,編譯器理所當然的認為你構造一個結構體資料型別是用來打包一些資料成員的,而最小的資料成員需要1個byte,編譯器為每個結構體型別資料至少預留1個byte的空間。所以,空結構體的大小就定位1個byte。

8. 大端與小端

在x86 系統下,輸出的值為多少?

#include

int main()

{

int a[5]={1,2,3,4,5};

int *ptr1=(int *)(&a+1);

int *ptr2=(int *)((int)a+1);

printf("%x,%x",ptr1[-1],*ptr2);

return 0;

}

5和0x02000000

由於x86是小端方式,所以低位內容存放到了低位地址。圖中每一種顏色代筆一個int型的記憶體分佈。&a可以獲得陣列a的地址,也就是這兒的0xbfd46624, 所以&a+1的結果應該是0xbfd46638(即圖中最下面紅色部分)。對於程式碼中的ptr1由於其為int型指標,所以ptr[-1]的意思應該是取0xbfd46638地址之前的一個整型,即為a陣列中的最後一個值5。而在計算ptr2的時候,(int)a是將整型地址a轉換成了一個整型數,這樣(int)a+1的結果就是0xbfd46625,然後再將其轉化為int型指標,這樣利用ptr2獲得的數值就是從0xbfd46625開始的一個整型,即為0x02000000

10. 花括號

花括號每個人都見過,很簡單吧。但曾經有一個學生問過我如下問題:

char a[10] = {“abcde”};

他不理解為什麼這個表示式正確。我讓他繼續改一下這個例子:

char a[10] { = “abcde”};

問他這樣行不行。那讀者以為呢?為什麼?

花括號的作用是什麼呢?我們平時寫函式,if、while、for、switch 語句等都用到了它,但有時又省略掉了它。簡單來說花括號的作用就是打包。你想想以前用花括號是不是為了把一些語句或程式碼打個包包起來,使之形成一個整體,並與外界絕緣。這樣理解的話,上面的問題就不是問題了。

11.再論 a 和&a 之間的區別

int main()

{

char a[5]={'A','B','C','D'};

char (*p3)[5] = &a;

char (*p4)[5] = a;

return 0;

}

int main()

{

char a[5]={'A','B','C','D'};

char (*p3)[3] = &a;

char (*p4)[3] = a;

return 0;

}

int main()

{

char a[5]={'A','B','C','D'};

char (*p3)[10] = &a;

char (*p4)[10] = a;

return 0;

}

int a[5][5];

int (*p)[4];

p = a;

問&p[4][2] - &a[4][2]的值為多少?

12. 用 malloc 函式申請 0 位元組記憶體

另外還有一個問題:用 malloc 函式申請 0 位元組記憶體會返回 NULL 指標嗎?

可以測試一下,也可以去查詢關於 malloc 函式的說明文件。申請 0 位元組記憶體,函式並不返回 NULL,而是返回一個正常的記憶體地址。但是你卻無法使用這塊大小為 0

的記憶體。這好尺子上的某個刻度,刻度本身並沒有長度,只有某兩個刻度一起才能量出長度。對於這一點一定要小心,因為這時候 if(NULL != p)語句校驗將不起作用。

13. 不使用任何變數編寫 strlen 函式

看到這裡,也許有人會說,strlen 函式這麼簡單,有什麼好討論的。是的,我相信你能 熟練應用這個函式,也相信你能輕易的寫出這個函式。但是如果我把要求提高一些呢:

不允許呼叫庫函式,也不允許使用任何全域性或區域性變數編寫 int my_strlen (char *strDest); 似乎問題就沒有那麼簡單了吧?這個問題曾經在網路上討論的比較熱烈,我幾乎是全程“觀戰” ,差點也忍不住手癢了。不過因為我的解決辦法在我看到帖子時已經有人提出了, 所以作罷。

解決這個問題的辦法由好幾種,比如巢狀有編語言。因為嵌套匯編一般只在嵌入式底 層開發中用到,所以本書就不打算討論 C 語言嵌套匯編的知識了。 有興趣的讀者,可以查詢相關資料。 也許有的讀者想到了用遞迴函式來解決這個問題。是的,你應該想得到,因為我把這 個問題放在講解函式遞迴的時候討論。

既然已經有了思路, 這個問題就很簡單了。

程式碼如下:

int my_strlen( const char* strDest )

{

assert(NULL != strDest);

if ('' == *strDest)

{

return 0;

}

else

{

return (1 + my_strlen(++strDest));

}

}

第一步:用 assert 宏做入口校驗。

第二步:確定引數傳遞過來的地址上的記憶體儲存的是否為''。如果是,表明這是一個 空字串,或者是字串的結束標誌。

第三步:如果引數傳遞過來的地址上的記憶體不為'',則說明這個地址上的記憶體上儲存 的是一個字元。既然這個地址上儲存了一個字元,那就計數為 1,然後將地址加 1 個 char型別元素的大小,然後再呼叫函式本身。如此迴圈,當地址加到字串的結束標誌符''時, 遞迴停止。

當然,同樣是利用遞迴,還有人寫出了更加簡潔的程式碼:

int my_strlen( const char* strDest )

{

return *strDest?1+strlen(strDest+1):0;

}

這裡很巧妙的利用了問號表示式, 但是沒有做引數入口校驗, 同時用*strDest 來代替('' == *strDest)也不是很好。所以,這種寫法雖然很簡潔,但不符合我們前面所講的編碼規範。

可以改寫一下:

int my_strlen( const char* strDest )

{

assert(NULL != strDest);

return ('' != *strDest)?(1+my_strlen(strDest+1)):0;

}

上面的問題利用函式遞迴的特性就輕易的搞定了, 也就是說每呼叫一遍 my_strlen 函式, 其實只判斷了一個位元組上的內容。但是,如果傳入的字串很長的話,就需要連續多次函式呼叫,而函式呼叫的開銷比迴圈來說要大得多,所以,遞迴的效率很低,遞迴的深度太大甚 至可能出現錯誤(比如棧溢位) 。所以,平時寫程式碼,不到萬不得已,儘量不要用遞迴。即便是要用遞迴,也要注意遞迴的層次不要太深,防止出現棧溢位的錯誤;同時遞迴的停止條 件一定要正確,否則,遞迴可能沒完沒了。

c語言讀書筆記

我有一本c語言程式設計書,已經靜靜的躺著好長時間了,前幾天朋友說,你這本書有些發黃了,看來是好長時間沒有翻過了。

其實這本書是我初學電腦時朋友給的,但只看了幾頁就放在那兒了,那時沒有自己的電腦,看起來很吃力。在朋友的提醒下,現在可以找一點時間去翻一翻,雖然沒有什麼價值,但學習也是一種樂趣......

記錄第二天:

昨天詳細讀了一下第一章,感覺寫的很詳細,但我不知道書上的知識點是否全正確。

一、資料型別的.基本概念

(1)資料型別規定了一個以值為其元素的集合。

(2)資料型別定義了一個運算集

(3)資料型別定義了資料在計算機內的儲存及書寫中的表示方式。

二、c語言的資料型別

資料包含常量和變數,他們都屬於某個資料型別。

三、常量

常量是程式中其值不發生變化的量,常量有數、字元和字串。在c中常量不需要型別說明就可以直接使用,除此之後c中還有一種表示常量的形式,稱為符號常量。

(一)數

1、c中數有整數和小數

(1)整數

整數有十進位制,八進位制,十六進位制,八進位制以數字0開頭,十六進位制以0x開頭,十進位制以1-9中的一個開頭。

整數有正負之分,分別在前面加上+,-符號,正數的符號+可以省略。

整數有短整數,整數,長整數之分,一般短整數16位,在我的機器上是short(16),int(32),long(32),對於整數的取值範圍有符號的(-2的n位次方至2的n位次方減1),無符號的0到2的n位次方-1

長型數的書寫方法:在數後面加一個L,如:0xffL

(2)實數

又叫浮點數,只有十進位制的,實數有單精度和雙精度之分,實數有兩種表示形式,一種是小數,一種是指數。

小數:由整數部分,小數點,小數部分。

指數:由尾數(必須有),e/E,指數(必須是整數)

(二)字元常量

字元常量是用一對''括起來的單一字元,一個字元常量在計算機中佔一個位元組(8位),字元常量的值就是這個字元在所屬字符集中的編碼(如:ASCII碼)。

字元常量中的單引號用作定界符,所以對於單引號的表示方法就得用另外一種方法表示:''' 用一個斜槓加一個'來轉義一個單引號,對於斜框用來轉義。

字元常量在計算機中是以編碼的方式存放,所以說他實際是一個位元組的整數,可以參與各種運算,如+,-,*,/,比較等。

(三)字串常量

字串常量是用雙引號護起來的一串字元,字元的個數稱其長度,字串常量簡稱為字串。

長度為n的字串在計算機的儲存中佔用n+1個位元組,最後多出來的那個存放一個NULL字元,ASCII編碼是0,我們可以用''來表示這個字元。

任何一個字串在機器內都是以結尾。如:abc,其實是'a','b','c',''的儲存格式。

對於"這個雙引號,由於用作定界符了,所以只能轉義表示:"。

注意: 'A' , "A"這是兩個不同的儲存方式,一個是65,一個是65,0

(四)

一般的字元可以直接寫出,但對於NULL,回車,新行,退格等字元如何書寫?

對於這些字元我們可以用其它的方式表示出來,常用的有:

新行

回車

水平製表

v 垂直製表

退格

f 換頁

a 響鈴

" 雙引號

' 單引號

NULL

ddd 1到3位八進位制數表示一個字元

xdd 1到2位十六進位制數表示一個字元

其實ddd,xdd就是字元的編碼。

(五)符號常量

在c語言中可以對常量進行命名,即用符號代替常數值,該符號叫符號常量,符號常量一般用大寫字母表示。

定義格式:

#define 符號常量名 常量

如:

#define NULL 0

#define EOF -1

#define PI 3.1415926

這裡#define是預編譯命令,每一個#define只能定義一個符號常量,且用一行書寫,不用分號結尾。

例:

#include

#define PI 3.1415926

int main(void)

{

float area,r;

printf("請輸入圓的半徑:");

scanf("%f",&r);

area = PI*r*r;

printf("這個圓的面積是:%f", area);

return 0;

}

使用符號常量的好處:

(1)增強程式的可讀性。

如用PI,代表數學中的PI,用EOF代表檔案尾,很直觀。

(2)增強程式的可維護性

如果多處用到同一個常量,可以定義符號常量,維護時只修改一處即可。對於擴充和可移植一個程式時大有好處。

四、變數

變數是他的值可以發生變化的一種量,每一個變數都對應計算機中相應長度的儲存單元,以存放變數所取的值。

如:

#include

int main(void)

{

int x = 0; //x的初值為0

x = 5; //x的值發生了變化

x = 7*5;

x = x+1;

printf("%d",x);

return 0;

}

每一個變數都用一個名字(叫變數名)來表示,變數的名字實際上是記憶體單元的命名,變數的地址就是該記憶體單元的開始地址。變數的命名規則同用戶自定義識別符號。

任何一個變數都屬於某一資料型別,如果他是整型量,則只能取整數值。

(一)基本資料型別

(1)、從長度上分有8位、16位、32位和64們珠。

(2)、從資料的符號來分,有無符號的,和有符號的。

(3)、按照數學性質來分,分為整型和實型。

c語言的基本資料型別表:

對於數的值域範圍的演算法:

有符號:[-2(位長度-1)次方到 2(位長度-1)次方-1]

無符號:[0到 2位長度次方]

-----------------------------------------------------------------

型別識別符號 名字 長度 範圍

char 字元型 8 ASCII字元程式碼

unsigned char 無符號字元型 8 0-255

signed char 有符號字元型 8 -127:127

int 整型 16(和環境有關) 範圍演算法

unsigned int 無符號整型 同上 同上

signed int 有符號整數 同上 同上

short int 短整型 16(和環境有關) 同上

unsigned short int 無符號短整型 同上 同上

signed short int 有符號短整型 同上 同上

long int 長整型 32位(和環境有關) 同上

unsigned long int 無符號長整型 同上 同上

signed long int 有符號長整型 同上 同上

float 單精度浮點型 32

double 雙精度浮點型 64

void 空型別

-------------------------------------------------------------------

注:

(1)void型別有兩種用法,一是指定函式的返回值的型別,一是用來設定類屬指標。

(2)對於不同的環境,資料表示的範圍不同,可以用sizeof(型別)來測試。

(二)變數的定義

變數的定義就是按照特定的方式為其指定標識,型別和長度等。程式中所用的每一個變數都必須先定義後引用,沒有定義的變數是不能引用的。定義的目的是為編譯程式提供所需的資訊,以保證完成以下工作。

1、在編譯時根據型別資訊來檢查程式中有無非法的資料結構。

2、根據型別資訊來檢查對變數施加的運算是否合理。如對兩個浮點數不可以進行%運算。

3、編譯時根據型別和長度資訊對變數分配記憶體,確定資料在記憶體中的表示方式。

定義:

資料型別 變數或變量表;

如:

int year,date;

int x;

(三)變數的初始化

int x; 對於這樣的變數只是指定了名字和資料型別,並沒有給他初始值,但這並不表示變數沒有值,他的值是當前記憶體單元中的資料,這個資料是沒有意義的,引用時會產生莫名其妙的結果。

初始化:在定義時直接賦值,則稱為初始化。

int x = 5;

double y = 3.4;

變數的初始化並不都是在編譯階段完成的,只有靜態變數和外部變數是在編譯階段完成,而區域性變數是在執行時初始化的。區域性變數的初始化就是一個賦值語句。

int abc()

{

int a =3;

int b =4; //這是在執行時才初始化,而非編譯時。

}

//補充

資料型別的轉換

在c語言的表示式中,准許對不同型別的數值型資料進行某一操作,當不同型別的資料進行操作時,先將其轉換成相同的資料型別,然後再進行操作。

有兩種轉換方式:隱式轉換,顯示轉換。

一、隱式型別轉換

隱式轉換就是在編譯時由編譯程式按照一定規則自動完成。c語言規定的轉換規則是由低階向高階轉換,如果一個運算子帶有兩個不同型別的運算元,那麼在操作之前先將較低的型別向高階轉換,再進行運算。

注意:

1、所有的float型別都轉換成double型別,提高運算精度。

2、在賦值語句中,如果=號左右兩邊的資料型別不同,則將賦值號右邊的值轉換為賦值號左邊的資料型別。

3、在函式引數傳遞時,也發生資料型別轉換。

4、char short自動轉換成int再參與運算。

規則:

1、char short自動轉換成int

2、float轉換成double

3、低階到高階-> char short ->int ->unsigned-> long ->(float ->double)double

二、顯式轉換

顯式轉換又叫強制型轉換,他不換預設規則,由程式設計師指定。

如:

int i;

i = i+3.124; //預設是i轉成double,再運算,之後double轉成int賦給i

顯示轉換: i = i+(int)3.124 //先把3.124轉成int,再參與運算。

顯示型別轉換方法:(資料型別)表示式

如:(int) 3.14159; //把double -> int