《C語言深度解剖》--學習筆記

1、什麼是定義,什麼是聲明

什麼是定義?什麼是聲明?它們有何區別?
舉個例子:
A)int i;
B)extern int i;(關於extern,後面解釋)
什麼是定義:所謂的定義就是(編譯器)創建一個對象,爲這個對象分配一塊內存並給它取上一個名字,這個名字就是我們經常所說的變量名或對象名。但注意,這個名字一旦和這塊內存匹配起來(可以想象是這個名字嫁給了這塊空間,沒有要彩禮啊。^_^),它們就同生共死,終生不離不棄。並且這塊內存的位置也不能被改變。一個變量或對象在一定的區域內(比如函數內,全局等)只能被定義一次,如果定義多次,編譯器會提示你重複定義同一個變量或對象。
什麼是聲明:有兩重含義,如下:
第一重含義:告訴編譯器,這個名字已經匹配到一塊內存上了(伊人已嫁,吾將何去何從?何以解憂,唯有稀粥),下面的代碼用到變量或對象是在別的地方定義的。聲明可以出現多次。
第二重含義:告訴編譯器,我這個名字我先預定了,別的地方再也不能用它來作爲變量名或對象名。比如你在圖書館自習室的某個座位上放了一本書,表明這個座位已經有人預訂,別人再也不允許使用這個座位。其實這個時候你本人並沒有坐在這個座位上。這種聲明最典型的例子就是函數參數的聲明,例如:
void fun(int i, char c);
好,這樣一解釋,我們可以很清楚的判斷:A)是定義;B)是聲明。
那他們的區別也很清晰了。記住,定義聲明最重要的區別:定義創建了對象併爲這個對象分配了內存,聲明沒有分配內存(一個抱伊人,一個喝稀粥。^_^)。

2、CPU處理數據的過程

數據從內存裏拿出來先放到寄存器,然後CPU 再從寄存器裏讀取數據來處理,處理完後同樣把數據通過寄存器存放到內存裏,CPU 不直接和內存打交道。

3、static關鍵字

(1)修飾變量

靜態全局變量,作用域僅限於變量被定義的文件中,其他文件即使用extern 聲明也沒法使用他。準確地說作用域是從定義之處開始,到文件結尾處結束,在定義之處前面的那些代碼行也不能使用它。想要使用就得在前面再加extern *。噁心吧?要想不噁心,很簡單,直接在文件頂端定義不就得了。

靜態局部變量,在函數體裏面定義的,就只能在這個函數裏用了,同一個文檔中的其他函數也用不了。由於被static 修飾的變量總是存在內存的靜態區,所以即使這個函數運行結束,這個靜態變量的值還是不會被銷燬,函數下次使用時仍然能用到這個值。

(2)修飾函數

函數前加static 使得函數成爲靜態函數。但此處“static”的含義不是指存儲方式,而是指對函數的作用域僅侷限於本文件(所以又稱內部函 數)。注意此時, 對於外部(全局)變量, 不論是否有static限制, 它的存儲區域都是在靜態存儲區, 生存期都是全局的. 此時的static只是起作用域限制作用, 限定作用域在本模塊(文件)內部.

使用內部函數的好處是:不同的人編寫不同的函數時,不用擔心自己定義的函數,是否會與其它文件中的函數同名。

4、如何用程序確認當前系統的存儲模式

(1)大端模式(Big_endian):字數據的高字節存儲在低地址中,而字數據的低字節則存放在高地址中。

(2)小端模式(Little_endian):字數據的高字節存儲在高地址中,而字數據的低字節則存放在低地址中。

union 型數據所佔的空間等於其最大的成員所佔的空間。對union 型的成員的存取都是相對於該聯合體基地址的偏移量爲0 處開始,也就是聯合體的訪問不論對哪個變量的存取都是從union 的首地址位置開始。

int checkSystem( )  
{  
    union check  
    {  
       int i;  
       char ch;  
    } c;  
    c.i = 1;  
    return (c.ch ==1);  
}  

5、文件包含

(1)#include < filename >

其中,filename 爲要包含的文件名稱,用尖括號括起來,也稱爲頭文件,表示預處理到系統規定的路徑中去獲得這個文件(即C 編譯系統所提供的並存放在指定的子目錄下的頭文件)。找到文件後,用文件內容替換該語句。

(2)#include “filename”

其中,filename 爲要包含的文件名稱。雙引號表示預處理應在當前目錄中查找文件名爲filename 的文件,若沒有找到,則按系統指定的路徑信息,搜索其他目錄。找到文件後,用文件內容替換該語句。

特別注意:
由於嵌套包含文件的原因一個頭文件可能會被多次包含在一個源文件中條件指示符可防止這種頭文件的重複處理。例如:

#ifndef BOOKSTORE_H
#define BOOKSTORE_H 
#endif

條件指示符#ifndef 檢查BOOKSTORE_H 在前面是否已經被定義,這裏BOOKSTORE_H 是一個預編譯器常量,習慣上預編譯器常量往往被寫成大寫字母,如BOOKSTORE_H 在前面沒有被定義則條件指示符的值爲真於是從#ifndef 到#endif 之間的所有語句都被包含進來進行處理。相反,如果#ifndef 指示符的值爲假則它與#endif 指示符之間的行將被忽略。

6、內存對齊

爲什麼會有內存對齊?

原因在於,8位CPU訪問數據一般是一次讀取八位,爲了訪問未對齊的內存,處理器需要作兩次內存訪問;然而,對齊的內存訪問僅需要一次訪問。

struct TestStruct1
{
      char c1;
      short s;
      char c2;
      int i;
};

編譯器默認將結構、棧中的成員數據進行內存對齊。因此,上面的程序輸出就變成了:c1 00000000, s 00000002, c2 00000004, i 00000008。編譯器將未對齊的成員向後移,將每一個都成員對齊到自然邊界上,從而也導致了整個結構的尺寸變大。儘管會犧牲一點空間(成員之間有部分內存空閒),但提高了性能。也正是這個原因,我們不可以斷言sizeof(TestStruct1)的結果爲8。在這個例子中,sizeof(TestStruct1)的結果爲12。

如何避免內存對齊的影響?

struct TestStruct2
{
       char c1;
       char c2;
       short s;
       int i;
};

這樣一來,每個成員都對齊在其自然邊界上,從而避免了編譯器自動對齊。在這個例子中,sizeof(TestStruct2)的值爲8。

7、&a[0]、&a 、a的區別

這裏&a[0]和&a 到底有什麼區別呢?

a[0]是一個元素,a 是整個數組,雖然&a[0]和&a的值一樣,但其意義不一樣。前者是數組第一個元素的首地址,而後者是數組的首地址。a 其意義與&a[0]是一樣,代表的是數組第一個元素的地址。(最主要的區別體現在 &a+1 和 a+1兩者的最後結果)

a作爲左值和右值得區別:

簡單而言,出現在賦值符“=”右邊的就是右值,出現在賦值符“=”左邊的就是左值。比如,x=y。
左值:在這個上下文環境中,編譯器認爲x 的含義是x 所代表的地址。這個地址只有編譯器知道,在編譯的時候確定,編譯器在一個特定的區域保存這個地址,我們完全不必考慮這個地址保存在哪裏。
右值:在這個上下文環境中,編譯器認爲y 的含義是y 所代表的地址裏面的數據。這個內容是什麼,只有到運行時才知道。

當a 作爲右值的時候代表的是什麼意思呢?很多書認爲是數組的首地址,其實這是非常錯誤的。a 作爲右值時其意義與&a[0]是一樣,代表的是數組首元素的首地址,而不是數組的首地址。這是兩碼事。但是注意,這僅僅是代表,並沒有一個地方(這只是簡單的這麼認爲,其具體實現細節不作過多討論)來存儲這個地址,也就是說編譯器並沒有爲數組a分配一塊內存來存其地址,這一點就與指針有很大的差別。
a 作爲右值,我們清楚了其含義,那作爲左值呢?
a 不能作爲左值!這個錯誤幾乎每一個學生都犯過。編譯器會認爲數組名作爲左值代表的意思是a 的首元素的首地址,但是這個地址開始的一塊內存是一個總體,我們只能訪問數組的某個元素而無法把數組當一個總體進行訪問。所以我們可以把a[i]當左值,而無法把a當左值。其實我們完全可以把a 當一個普通的變量來看,只不過這個變量內部分爲很多小塊,我們只能通過分別訪問這些小塊來達到訪問整個變量a 的目的。

8、以指針的形式訪問和以下標的形式訪問

A)
char *p = “abcdef”;
B)
char a[] = “123456”;

(1)以指針的形式訪問和以下標的形式訪問指針

例子A)定義了一個指針變量p,p 本身在棧上佔4 個byte,p 裏存儲的是一塊內存的首地址。這塊內存在靜態區,其空間大小爲7 個byte,這塊內存也沒有名字。對這塊內存的訪問完全是匿名的訪問。比如現在需要讀取字符‘e’,我們有兩種方式:

1)以指針的形式:*(p+4)。先取出p 裏存儲的地址值,假設爲0x0000FF00,然後加上4 個字符的偏移量,得到新的地址0x0000FF04。然後取出0x0000FF04 地址上的值。

2)以下標的形式:p[4]。編譯器總是把以下標的形式的操作解析爲以指針的形式的操作。p[4]這個操作會被解析成:先取出p 裏存儲的地址值,然後加上中括號中4 個元素的偏移量,計算出新的地址,然後從新的地址中取出值。也就是說以下標的形式訪問在本質上與以指針的形式訪問沒有區別,只是寫法上不同罷了。

(2)以指針的形式訪問和以下標的形式訪問數組

例子B)定義了一個數組a,a 擁有7 個char 類型的元素,其空間大小爲7。數組a 本身在棧上面。對a 的元素的訪問必須先根據數組的名字a 找到數組首元素的首地址,然後根據偏移量找到相應的值。這是一種典型的“具名+匿名”訪問。比如現在需要讀取字符‘5’,我們有兩種方式:

1)以指針的形式:*(a+4)。a 這時候代表的是數組首元素的首地址,假設爲0x0000FF00,然後加上4 個字符的偏移量,得到新的地址0x0000FF04。然後取出0x0000FF04 地址上的值。

2)以下標的形式:a[4]。編譯器總是把以下標的形式的操作解析爲以指針的形式的操作。a[4]這個操作會被解析成:a 作爲數組首元素的首地址,然後加上中括號中4 個元素的偏移量,計算出新的地址,然後從新的地址中取出值。

另外一個需要強調的是:上面所說的偏移量4 代表的是4 個元素,而不是4 個byte。只不過這裏剛好是char 類型數據1 個字符的大小就爲1 個byte。記住這個偏移量的單位是元素的個數而不是byte 數,在計算新地址時千萬別弄錯了。

9、地址的強制轉換

(unsigned long)p + 0x1 的值呢?這裏涉及到強制轉換,將指針變量p 保存的值強制轉換成無符號的長整型數。任何數值一旦被強制轉換,其類型就改變了。所以這個表達式其實就是一個無符號的長整型數加上另一個整數。所以其值爲:0x100001。
(unsigned int* )p + 0x1 的值呢?這裏的p 被強制轉換成一個指向無符號整型的指針。所以其值爲:0x100000+sizof(unsigned int)*0x1,等於0x100004。

10、二維數組在內存中的佈局

這裏寫圖片描述

以數組下標的方式來訪問其中的某個元素:a[i][j]。編譯器總是將二維數組看成是一個
一維數組,而一維數組的每一個元素又都是一個數組。a[3]這個一維數組的三個元素分別爲:
a[0],a[1],a[2]。每個元素的大小爲sizeof (a[0]),即sizof(char)*4。由此可以計算出a[0],a[1],a[2]
三個元素的首地址分別爲& a[0],& a[0]+ 1*sizof(char) * 4,& a[0]+ 2*sizof(char) * 4。亦即a[i]的首地址爲& a[0]+ i*sizof(char) * 4。這時候再考慮a[i]裏面的內容。就本例而言,a[i]內有4個char 類型的元素,其每個元素的首地址分別爲&a[i],&a[i]+1*sizof(char),&a[i]+2*sizof(char),&a[i]+3 * sizof(char),即a [i] [j]的首地址爲&a [i] +j*sizof(char)。再把&a[i]的值用a 表示,得到a[i] [j]元素的首地址爲:a+i* sizof(char)* 4+ j* sizof(char)。同樣,可以換算成以指針的形式表示:* (* (a+i)+j)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章