天天看點

面試大全 | C語言進階部分總結

一、記憶體大話題

1.0、記憶體就是程式的立足之地,展現記憶體重要性。

1.1、記憶體了解:

記憶體實體看是有很多個 Bank(就是行列陣式的存儲晶片),每一個 Bank 的列就是位寬 ,每一行就是 Words,則存儲單元數量=行數(words)×列數(位寬)×Bank的數量;通常也用 M×W 的方式來表示晶片的容量(或者說是晶片的規格/組織結構)。

M 是以位寬為機關的總容量,機關是兆 ,W 代表位寬, 機關是bit。計算出來的晶片容量也是以 bit 為機關,但使用者可以采用除以 8 的方法換算為位元組(Byte)。比如 8M×8,這是一個 8bit 位寬晶片,有 8M 個存儲單元,總容量是

64Mbit(8MB)

1.2、c語言中其實沒有 bool 類型:以 0 表示假,非0 表示真,則在記憶體存儲是以 int 型存放的。如果想要表示真假,可以用

int/char

型做替換,在 c++ 中就有

bool x=true/false

;

1.3、記憶體對齊:記憶體對齊(提高通路效率速度,編譯器一般預設是4位元組對齊)

1.4、

char/int/short/long/float/doubl

e型:放在記憶體的長度和解析作用。(

int *)0

,使0位址指向一個int型。又比如

0000111010101

可以解析成int型也可以解析成 float 型。

1.5、Linux 核心是面向對象的,而 c語言是面向過程的,但可以用結構體内嵌指針變成面向對象。如

struct student{ int age; //變量 int lenth; //将相當于一個類,有變量有函數 char *name; void (*eat)(void); //函數指針 }

1.6、棧的了解:

(1) 運作時自動配置設定 & 自動回收:棧是自動管理的,程式員不需要手工幹預。友善簡單。(表現在彙編代碼,編譯時,會自動編譯成彙編碼實作函數調用完立即改變棧頂)

(2) 反複使用:棧記憶體在程式中其實就是那一塊空間,程式反複使用這一塊空間。(硬體上有個寄存器,用來存放棧的棧頂位址,棧是有大小的空間)

(3) 髒記憶體:棧記憶體由于反複使用,每次使用後程式不會去清理,是以配置設定到時保留原來的值。

(4) 臨時性:(函數不能傳回棧變量的指針,因為這個空間是臨時的)

(5) 棧會溢出:因為作業系統事先給定了棧的大小,如果在函數中無窮盡的配置設定棧記憶體總能用完。棧的操作(怎麼出棧怎麼入棧)是由具體硬體來幹預,程式員隻要明白原理就可以了,但是要給相應的棧寄存器指派。當調用函數時,變量會自動放在棧中(入棧)當函數調用完後,棧會自動出棧.

( 6 ) 棧的 "發展"有四種情況,滿增棧,滿減棧,空增棧,空減棧,至于是那種要根據編譯器決定,而 s5pv21 是滿減棧。

1.7、堆的了解:

(1)作業系統堆管理器管理:堆管理器是作業系統的一個子產品,堆管理記憶體配置設定靈活,按需配置設定。

(2)大塊記憶體:堆記憶體管理者總量很大的作業系統記憶體塊,各程序可以按需申請使用,使用完釋放。

(3)髒記憶體:堆記憶體也是反複使用的,而且使用者用完釋放前不會清除,是以也是髒的。

(4)臨時性:堆記憶體隻在 malloc 和 free 之間屬于我這個程序,而可以通路。在 malloc 之前和 free 之後都不能再通路,否則會有不可預料的後果。

(5)程式手動申請&釋放:手工意思是需要寫代碼去申請 malloc 和釋放 free。(記住:不要把申請的位址給搞丢了, 不然自己用不了,也釋放不了)

申請一段記憶體,可以是:

m

alloc(10*sizeof ( int ) );
           

原型:

void *malloc(size_t size); 
           

//指針函數 

size_t

是宏定義

int

 都是便于可移植性 ,傳回一個記憶體位址,

void *

可以看出,希望申請的記憶體用來存放什麼就強制類型什麼。

calloc( 10,sizeof ( int ) );

 原型:

void *calloc(size_t nmemb, size_t size);

// nmemb 個單元,每個單元 size 位元組

void *realloc(void *ptr, size_t size);

// 改變原來申請的空間的大小的 ptr 是原來申請記憶體的指針,size 是想要重新申請記憶體的大小使用就是

*(p+1)=12 ;

*(P+3)=110;

申請失敗傳回 NULL,申請成功傳回一個位址,申請之後一定要檢驗

(NULL!=p)

用完一定要 

free ( p );

釋放後不是不能用,是不應該使用了。可以給它“洗盤子‘,

p=NULL;

其實申請的記憶體并不能真正改變大小,原理是先重新申請一段記憶體,然後把原來申請的記憶體上的内容複制到新的記憶體上,然後釋放掉原來的記憶體,傳回新的指針。

(6) 在申請記憶體時,

malloc(0)

其實也是成功的,因為系統規定少于一定數目的大小,都申請規定的大小,如在

win32

系統下申請少于 32 位元組的位址,最後申請到的空間是 32 位元組,在朱老師視訊中申請少于 16 位元組的位址,最後申請到的是 16 位元組,至于規定多少位元組,由具體的系統而言。

1.8、記憶體裡的資料:

(1)代碼段:存放代碼二進制、常量(

char *p="linux"

,則 ”linux“ 存放在代碼段,是不可更改的)

(2) 資料段: 存放非 0 全局變量、靜态局部變量(局部隻屬于函數的,不是整個程式的)

(3) bss : 存放為0的全局變量 / 為 0 的靜态局部變量、存放未初始化全局變量 / 靜态局部變量

注意:

const int a=9;

 有兩種存放方式:第一種确實存放在代碼段,讓a不能修改,第二種是仍然存放在資料段中,讓編譯器來判斷,如果有改變的代碼就會報錯。至于那種,是不确定的,像單片機就屬于第一種。

1.9、(1) 一個源檔案實際上是以段為機關編譯成連接配接成可執行檔案(a .out );這個可執行檔案總的說是分為資料段,代碼段,自定義段,資料段還可以細分成 .bbs 段。而雜段會在執行的時候拿掉。是以 a.out 分為雜段,資料段(存放的是非 0 全局變量).bbs 段,代碼段。

(2) 記憶體實際上被劃分了兩大區域,一個是系統區域,另一個是使用者區域,而每一個區域又被劃分成了幾個小區域,有堆,棧,代碼區,.bbs 區,資料區(存放的是非0全局變量)。

(3) 對于有作業系統而言, 當我們在執行 a.out 可執行檔案時,執行這個檔案的那套程式會幫我們把雜段清掉,然後把相應的段加載到記憶體對應的段。對于裸機程式而言,我們是使用一套工具将 a.elf 的可執行程式給清掉了所有段的符号資訊,把純淨的二進制做成 bin 格式的燒錄檔案。是以我們加載到記憶體的程式是連續的,也就是說代碼段和資料段、.bbs 段都是連續的。當然,棧空間是我們自己設定的。而且在裸機中我們不能使用 malloc 函數,因為我們使用的隻是編譯器、連接配接器工具沒有內建庫函數,沒有定義堆空間區。

(4) 大總結多程式運作情況:在 Linux 系統中運作 cdw1.out 時,運作這個檔案的那套程式會幫我們把相應的段加載到記憶體對應的段。然後作業系統會把下載下傳到記憶體的具體實體位址與每條指令(32位)的連結位址映射到 TTB 中(一段記憶體空間),當我們又運作 cdw2.out 時,同樣也像 cdw1.out 一樣加載進去,并映射到 TTB 表中。而且這兩個 .out 檔案預設都是連結 0 位址(邏輯),當 cpu 發出一個虛拟位址(Linux中程式邏輯位址)通過 TTB 查找的實體位址是不一樣的。是以對于每一個程式而言,它獨占 4G 的記憶體空間,看不到其他程式。

二、位操作

2.1 ~(0u) 是全 1;

2.2 位與 & 位或 | 位取反~ 位異或^

2.3、位與、位或、位異或的特點總結:

位與:(任何數,其實就是1或者0)與1位與無變化,與0位與變成0 位或:(任何數,其實就是1或者0)與1位或變成1,與0位或無變化 位異或:(任何數,其實就是1或者0)與1位異或會取反,與0位異或無變化

2.4、左移位 << 與右移位 >> C語言的移位要取決于資料類型。對于無符号數,左移時右側補0(相當于邏輯移位) 對于無符号數,右移時左側補0(相當于邏輯移位) 對于有符号數,左移時右側補0(叫算術移位,相當于邏輯移位) 對于有符号數,右移時左側補符号位(如果正數就補0,負數就補1,叫算術移位)

2.5、小記:常與 1 拿來 做位運算。讓他取反、移位 得到想要的數。

2.6、直接用宏來置位、複位(最右邊為第1位)。置位置1,複位置0 ;

#define SET_NTH_BIT(x, n) (x | ((1U)<<(n-1)))
#define CLEAR_NTH_BIT(x, n) (x & ~((1U)<<(n-1)))
           

三、指針—精髓

3.1 

printf("%p \n");

 其中 %p 表示輸出一個指針,就是指針變量(其存放的那個位址),可以了解為輸出一個位址。

3.2 

int* p1, p2 ;

 等同于 

int *p1;

int p2

;  

int *p="Linux"

,其不能改變 *P ,因為 ”linux" 是一個常數。

3.3 ( 代碼規範性 )在定義指針時,同時指派為 NULL,在用指針時,先判斷它是不是 NULL 。尤其是在 malloc 申請記憶體後,free(p) ;則一定要讓 p=NULL

3.4 C/C++ 中對 NULL 的了解:

#ifdef _cplusplus// 定義這個符号就表示目前是C++環境
           
#define NULL 0;// 在C++中NULL就是0
#else
#define NULL (void *) 0;// 在C中NULL是強制類型轉換為void *的0
#endif
           

3.5、修飾詞:const (修飾變量為常量,應該了解為不應該去變它,當作常量,而并非永遠不能改變,當然要看具體運作環境,在 gcc,const 這種就可以采用指針方式修改,但是在在 VC6.6++ 中就不可以修改):其雖然是當作常數,但是仍然存放在資料段中,用指針仍然可以改變值。

//第一種:const int *p;
//第二種:int const *p;
//第三種:int * const p;
//第四種:const int * const p;
           

3.6、 數組 int a[2]; 其中a是指首元素的首位址,&a是整個數組的收位址(數組指針,其這個指針指向一個數組),他們的值是一樣的,但意義不一樣,可以參照 int a; int *p=&a; 來了解。數組和指針天生姻緣在于數組名;

int a[3]; int* p=a;是可以的,但是 int p=&a;就會報錯,盡管他們的值是一樣的,但意義不一樣,是以是不允許的,除非強制類型轉換。在通路時是a[0],其實編譯器會把它變成

(a+0)

的方式,隻是用 a[0] 看起來更友善,封裝了一下而已,實質還是指針。

3.7、 siziof() 是一個運算符,測試所占記憶體空間,如 

int a[100] ;``sizeof(a)=400;

與strlen( )要有所差別,他是測字元串實際長度的,不包括 ‘\0‘ ,如果給 strlen 傳的參數不是一個字元串,則它會一直去找,直到 找到第一個 ‘\0’,然後再計算其長度。

如 

char a[]="chen";

char *p=a;

 則

strlen(p)=4;

3.8、 當數組作為一個形參時,其實參是一個數組名(也可以是指針,其本質就是指針),意義是首元素的首位址,則傳過去隻影響形參的第一個元素。形參數組的位址被實參數組位址所綁定;

實參的大小會丢失,是以往往會傳一個 int num 大小進去。

3.9、 結構體做為形參時,應盡量用指針/位址方式來傳,因為結構體變量有時會占很大,效率很低。

3.10、 

int *p=&u;

 p 存放的是變量u的位址,而 &p 的意思就是變量 p 本身的位址。

3.11、當要傳參的個數比較多時,我們可以打包成一個結構體,傳參的個數越多,其開銷就更大.

3.12 一個函數作用其實就是輸入輸出,參數可以作為輸入,傳回可以作為輸出,但是當要傳回多個輸出時,這時候就不夠用了,是以常常傳回值用來判斷程式又沒有出錯,而參數就是當作輸入輸出的,輸入時可以加 const 表示它沒必要去修改,而輸出都是指針,因為要改變它的值,隻能采用位址傳遞這種方式。比如:

char *strcpy(char *dest,const char *src)

四、C語言複雜表達式

4.1、在表達式中,要看符号的優先級和結合性。

4.2、在了解記憶體時,記憶體0位址在最底下,至上位址逐漸增加。

4.3、

int *p;

 是定義的一指針變量 p,而

int ( *p)[4]

;也是一個指針變量p;也可以這樣想:凡是遇到(*p)什麼的判斷他是指針後,就可以說他是指針變量,包括函數指針。

4.4、一個函數 

int max(int a ,int b);

 則他的函數指針是 

int ( *p ) (int ,int );

其意思就是定義這個類型的函數指針變量 p; p=max 是指派,引用是p();則相當于 max()調用這個函數。

函數指針必須和原函數的類型一樣。

4.5 函數指針其實就是為了做結構體内嵌指針的,這樣就構成了進階語言中的類。再一個就是上述 4.4 中 p=&max;也是可以的,它和 p=max,值和意義都是一樣的,這個和數組有所差別,數組的 a 和 &a 的值雖然一樣,但是意義完全不一樣

。int a[4];

a 有兩層意思,第一層是數組名,&a 表示整個數組的位址,第二層表示首元素的首位址。

4.6 

int (*p[4])(int ,int)

其意思是函數指針數組,一個 4 長度的數組,裡面存了4個函數指針。

4.7 printf 在做輸出時,其機制是緩沖行來輸出,即當遇到一個\n後再列印出來,即使再多 printf,沒有遇到 \n,都不是一個一個列印。

'\r'是回車,'\n'是換行,前者使光标到行首,後者使光标下移一格,通常敲一個Enter鍵,即是回車,又是換行(\r\n)。Unix中每行結尾隻有“<換行>,即“\n”;Windows中每行結尾是“<換行><回車>”,即“\r\n”;Mac中每行結尾是“<回車>”。scanf("");裡面不要加\n符。

4.8 在一個c檔案中,有時候會多次引入一個.h檔案,是以在寫.h檔案時,要寫

#ifndef _FINE_
#define _FINE_
XXXXXXXX
XXXXXXXXXXX
#endif 
           

4.9、

typedef int *int Type;

const intType p

,其意思是指針 p 為 const;

4.9.1 對于 typedef 的定義:如

typedef const int cdw;

 可以這樣了解,typedef 就是給一個類型差別名的,那麼系統會自動識别該類型,如果

typedef const int char

 則就報錯。

4.9.2 在開發中經常會

typedef int int32_t ;

typedef short int16_t;

 這樣做的目的是便于在不同平台下的移植,如果當在另一個平台下,int 是64位的,但是我的項目中都是用的int32_t;

是以隻需要修改int32_t就可以了,我可以讓他typedef short int32_t;這樣我隻更改一次,其餘的都改了,做到一改全改。

4.9.3 int **p; int *a[4]; p=a;可以這樣了解:首先它是指針數組,既然是數組,則a即表示數組名又表示首元素的首位址,a[0]是一個一重指針,而a是a[0]的位址,那麼a就是一個二重指針;{ 一重指針的位址就是二重指針變量,是以有p=a; 而 int a[4][3] ,a和一維數組的意思是一樣的,如 int a[3][6],int *p ;p=a[0];是以不能p=a,int *a[3][3],int **p;p=a[0];}

4.9.4、二維數組是為了簡化程式設計,平面型。數組以下标示方式通路其實是編譯器封裝起來的,實質是指針通路。int (*p)[5]; int a[2][5];則有 p=a; 關鍵是要把二維數組抽象成 n 行 n 列用指針通路方式了解:二維數組可以看作是一個方格子的矩陣,比如 a[2][5],那麼就是 2 行 5 列的 10 個小格子,第一行可以收納起來變成一個指向一維數組的指針,第二行也是如此;

這樣收納後就變成了一個新的數組 a[2],每一個格子存放的是它收納的第一個元素的位址,如 a[0] 存放的是第一行第一列元素的位址,“a”[1]存放的是第二行第一列的位址;

再與一維數組相聯系,一維數組名即表示數組名又表示數組第一個元素的位址,是以 a[2][5] 中的 a 表示 “a"[2] 數組第一個元素的位址;那麼再把 p=a;層層推遞,(p+i)表示指向第幾行的位址,(p+i)表示取第幾行的值(而這個值存放的是第幾行一列元素的首位址),(p+i)+j 表示指向第幾行第幾列的位址,最後在引用這個位址,((p+i)+j)就表示第幾行第幾列的值了。

一重指針----------->一維數組

二重指針----------->指針數組

數組指針----------->二維數組

函數指針----------->普通函數

五、數組&字元串&結構體&共用體&枚舉

5.1、c語言中定義一個字元串: 

char a[6]={'l','i','n','u','x','\0'};

  '\0' 的字元編碼為 0 就是 NULL;也就是說記憶體中遇到 0,翻譯成字元是就是 '\0' ,或這是 NULL;

char a[6]="linux";
char *p="linux";
           

5.2、 sizeof(a)=6 是運算符,其意思是所占空間大小,包括字元串後面的‘\0',strlen(a)=5 是一個函數,其意思是字元串的長度。strlen( p);其中 p 隻有是字元指針變量才有意義,它的形參是數組變量是沒有意義的,因為 strlen 是根據什麼時候遇到 '\0',才結束測試字元串的長度,就算數組不初始化也是有長度的。

char *p="linux";

 sizeof(p) 永遠等于 4,因為 p 是指針變量,存的是位址。是以總結:sizeof() 是拿來測數組的大小,

strlen()

是拿來測試字元串的長度。

5.3、結構體用 . 或者是 -> 通路内部變量,其實質是用的指針通路。如

struct student{
int a;
double b;
char c;
}s1;
           

則 s1.a =12; 實質就是int *p=(int *) &s1;*p=12 首先a是int 型,是以是強制類型 int * ,其次是就是算位址,然後強制類型,位址應該是int 型然後加減,不然的話,系統s1.b=12.2;實質就是double *p= (double *) ((int)&s1+4),*p=12.2; 不知道是以int 型加減還是以float型加減,還是以char型加減,是以 應當 (int)&s1; 而且因為位址是s1.c=c;實質就是 char *p=(char *) ((int)&s1+12); *p=c; 4位元組的,是以必須是int型。

5.4、對齊方式:

(1)猜測如果是 32 位系統,那麼編譯器預設是 4 位元組對齊,64 位系統,那麼編譯器預設是 8 位元組對齊,因為 32 位或 64 位一次性通路效率是最高的。

(2)

<1>結構體首位址對齊(編譯器自身幫我們保證,會給它配置設定一個對齊的位址,因為結構體自身已經對齊了,那麼第一個變量也就自然對齊,是以我們才可以想象成第一個變量從0位址存放);

<2>結構體内部的各個變量要對齊。

<3>整個結構體要對齊,因為定義結構體變量 s1 時,再定義變量 s2 時,如果 s1 沒有對齊,就坑了s2,是以也要保證整個結構體對齊。

無論是按照幾位元組對齊,我們都可以聯想到記憶體實際的安排。1位元組對齊那麼不管int float double 類型,在每4個格子的記憶體挨着存放。2位元組對齊,也是一樣的想法,舉一個列子,如果第一個變量是char 型,第二個變量是int型,那麼0位址存放char型,1位址空着,2位址存放int型位址部分,3位址存放int型位址部分,然後上排最右4、5位址存放int型高址部分。4位元組對齊,如果第一個變量是char型,第二個變量是int型,那麼0位址存放char型,1,2,3位址空着,從4位址開始存放int,最後給變量配置設定完記憶體空間後,必須要保證整個結構體對齊,下一個結構體的存放起始位址是n位元組對齊整數倍,如是4位元組對齊,那麼最後short算成4位元組 以保證整個結構體對齊。

整個結構體對齊,如2位元組對齊(2的整數倍),隻要是0、2、4位址就行了,如果是4位元組對齊(4的整數倍),就必須是0、4位址。8位元組對齊(8的整數倍)

(3)猜測4位元組/8位元組其實是針對int型/double型的,比如0位址是char型,那麼4位元組對齊,int型、float型就必須從4位址開始存放,那麼8位元組對齊,int型就必須從4位址存放,double型就必須從8位址開始存放.小于幾位元組對齊的那些,如char型和short型隻要能按照規則存放就行了。

(4)對齊指令:<1>需要#prgama pack(n)開頭,以#pragma pack()結尾,定義一個區間,這個區間内的對齊參數就是n。(不建議使用)

如:s1占5個位元組,s2占8位元組(預設)

#pragma pack(1)
struct stu1
{
           

(結構體本身以及變量) 對齊規則:2位元組對齊(2的整數倍),隻要是0、2、4位址就行了,4位元組對齊(4的整數倍),就必須是0、4位址, 8位元組對齊(8的整數倍),就必須是0、8、16

char c;
int a;
//short d;
}s1;

struct stu2
{

char c;
int a;
//short d;
}s2;
           

<2> gcc 推薦的對齊指令

__attribute__((packed)) __attribute__((aligned(n)))

,在 VC 中就不行,沒有定義這個指令

(1)

__attribute__((packed))

使用時直接放在要進行記憶體對齊的類型定義的後面,然後它起作用的範圍隻有加了這個東西的這一個類型。packed的作用就是取消對齊通路。

(2)

__attribute__((aligned(n)))

使用時直接放在要進行記憶體對齊的類型定義的後面,然後它起作用的範圍隻有加了這個東西的這一個類型。它的作用是讓整個結構體變量整體進行n位元組對齊(注意是結構體變量整體n位元組對齊,而不是結構體内各元素也要n位元組對齊,内部元素按照預設對齊方式)

例子:

struct mystruct11
{// 1位元組對齊4位元組對齊
int a;// 44
char b;// 12(1+1)
short c;// 22
}__attribute__((packed));

typedef struct mystruct111
{// 1位元組對齊4位元組對齊2位元組對齊
int a;// 44 4
char b;// 12(1+1)2
short c;// 22 2
short d;// 2 4(2+2)2
}__attribute__((aligned(1024))) My111;
           

5.5、offsetof 宏:

#define offsetof( TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)

(1)offsetof 宏的作用是:用宏來計算結構體中某個元素和結構體首位址的偏移量(其實質是通過編譯器來幫我們計算)。

(2)offsetof 宏的原理:我們虛拟一個 type 類型結構體變量,然後用 type.member 的方式來通路那個 member 元素,繼而得到 member 相對于整個變量首位址的偏移量。

(3)學習思路:第一步先學會用 offsetof 宏,第二步再去了解這個宏的實作原理。

(TYPE *)0 這是一個強制類型轉換,把0位址強制類型轉換成一個指針,這個指針指向一個TYPE類型的結構體變量。(實際上這個結構體變量可能不存在,但是隻要我不去解引用這個指針就不會出錯)。

((TYPE *)0)->MEMBER(TYPE *)0是一個TYPE類型結構體變量的指針,通過指針指針來通路這個結構體變量的member元素,然後對這個元素取位址,又因為改位址是從0位址開始算的,是以這個位址就是相對起始位址的偏移量。

5.6 container_of宏:#define container_of(ptr, type, member) ({

const typeof(((type *)0)->member) * __mptr = (ptr);

(type *)((char *)__mptr - offsetof(type, member)); }) 兩條語句;,然後用{ } ,\表示提示編譯器本行因為螢幕不夠,連結下一行。用#(也就是宏定義)時,如果本行不夠要用 \ 提示編譯器接着是下一行的。必須要用 \ ,猜測因為宏定義一行就算結束了。

(1)作用:知道一個結構體中某個元素的指針,反推這個結構體變量的指針。有了container_of宏,我們可以從一個元素的指針得到整個結構體變量的指針,繼而得到結構體中其他元素的指針。

(2)typeof關鍵字的作用是:typepef(a)時由變量a得到a的類型,typeof就是由變量名得到變量資料類型的。

(3)這個宏的工作原理:先用typeof得到member元素的類型定義成一個指針,然後用這個指針減去該元素相對于整個結構體變量的偏移量(偏移量用offsetof宏得到的),減去之後得到的就是整個結構體變量的首位址了,

再把這個位址強制類型轉換為type *即可。

5.7 p是一個位址,(int)p+6 和(char *)+6;效果是一樣的,第一種是将位址p當作int型加減,第二種是将位址p做為char *指針,他每次加減都是一位元組一位元組相加減的,如果是 (int)P+6,那麼他每次加減都是按照4位元組一跳。就相當于加了+46;

5.8 小端模式:變量的高位址存放在高位址,低位址存放在低位址;通信模式也要分大小端,先發送/接受的是高位址還是低位址,大端模式:變量的高位址存放在低位址,低位址存放在高位址;

測試:有用共用體 union 和指針方式來測試,基本思路是讓 int a=1; 看低位址 char 是0還是1 ;變量都是從位址開始存放,隻是變量的高位址和低位址先存放誰不确定。

不能用位與來測,因為存放和讀取都是按照某一個方式來的,結果永遠都是一樣的。int a=1; char b=(char)a;這種方式不可以測試,因為不管大小端,它都以變量a的低位址部分賦給b;

union stu{
int a; int ce( )
{
int a=1;
int b=*((char *)&a);
return b;
}
char b;
}
int ce( )
{
union stu s;
s.a=1;
return s.b;
}
           

5.9、枚舉類型(int型):這樣寫預設從第一個常量開始是0,1,2,3,4.........也可以自己指派,但是每一個值是不一樣的,否則邏輯上出錯。

e

num week{sunday, sunday=1,moday, moday=5,tuseday, 然後其他常量以此遞增。wenzeday,friday,saterday,}today; today=sunday;* // 錯誤1,枚舉類型重名,編譯時報錯:error: conflicting types for ‘DAY’typedef enum workday{MON, // MON = 1;TUE,WEN,THU,FRI,}DAY;typedef enum weekend{SAT,SUN,}DAY;*// /錯誤2,枚舉成員重名,編譯時報錯:redeclaration of enumerator ‘MON’typedef enum workday{MON, // MON = 1;TUE,WEN,THU,FRI,}workday;typedef enum weekend{MON,SAT,SUN,}weekend;}
           

六、C語言宏定義與預處理、函數和函數庫(看部落格strcyp原函數)

6.1、源碼.c->(預處理)->預處理過的 .i 檔案->(編譯)->彙編檔案.S->(彙編)->目标檔案.o->(連結)->elf可執行程式預處理用預處理器,編譯用編譯器,彙編用彙編器,連結用連結器,這幾個工具再加上其他一些額外的會用到的可用工具,合起來叫編譯工具鍊。gcc就是一個編譯工具鍊。

<1>預處理的意義(1)編譯器本身的主要目的是編譯源代碼,将C的源代碼轉化成.S的彙編代碼。編譯器聚焦核心功能後,就剝離出了一些非核心的功能到預處理器去了。

(1)預處理器幫編譯器做一些編譯前的雜事。如:(1)#include(#include <>和#include ""的差別)

(2)注釋

(3)#if #elif #endif#ifdef

(4)宏定義

備注:gcc中隻預處理不編譯的方法 -o生成可執行檔案名 -c隻編譯不連結 -E 隻預處理不編譯 -I ( 是大i,不是L )編譯時從某個路徑下尋找頭檔案 . /目前

(1)gcc編譯時可以給一些參數來做一些設定,譬如gcc xx.c -o xx可以指定可執行程式的名稱;譬如gcc xx.c -c -o xx.o可以指定隻編譯不連接配接,也可以生成.o的目标檔案。

(2)gcc -E xx.c -o xx.i可以實作隻預處理不編譯。一般情況下沒必要隻預處理不編譯,但有時候這種技巧可以用來幫助我們研究預處理過程,幫助debug程式。

(3)連結器:連結的時候是把目标檔案(二進制)通過有序的排列組合起來,如 star.s main.c led.c 這三個源檔案,分别被編譯成三個目标檔案 ,每個目标檔案有很多函數集合。連結的時候會根據運作思路把這些雜亂的函數給排列組合起來,不是把目标檔案簡單的排列組合。

(4)當生成可執行程式之後,這個可執行程式裡面有很多符号資訊,有個符号表,裡面的符号與一個位址相對應,如 函數名max對應一個位址,雖然這個程式有符号資訊,但是為什麼還是可以執行呢?因為如windows的exe程式,有專門的一套程式來執行這個.exe 檔案,就好比壓縮檔案,就有一套 “好壓”的軟體,然後去壓縮(執行).rar .zip的檔案,而這套程式就把這些符号資訊給過濾掉,然後得到純淨的二進制代碼,最後把他們加載到記憶體中去。

(5) debug版本就是有符号資訊,而Release版本就是純淨版本的。可用strip工具:strip是把可執行程式中的符号資訊給拿掉,以節省空間。(Debug版本和Release版本)objcopy:由可執行程式生成可燒錄的鏡像bin檔案。

6.2、預處理:

<1>頭檔案有”“是本目錄去找,找不到就去庫頭檔案找,和< > 隻到庫頭檔案去找,庫頭檔案可以自己制作,用 -I ( 是大i,不是L )參數去尋找路徑。

頭檔案在預處理時,會把檔案的内容原封不動的指派到 c 檔案裡面。

<2>注釋:在預處理時,把注釋全部拿掉。注意:#define zf 1 再判斷 #undef zf 2 時,也是通過的。其意思是有沒有定義過zf.

<3>條件編譯:當作一個配置開關 #define NUM 表示定義了NUM,則執行下一條語句,且NUM用空格替代,而且預處理會删掉條件編譯,留下正确的執行語句。

<4>宏定義:#define cdw 1 在預處理階段,會替代那些宏,可以多重替代宏;也可以表示多個語句,如 #define cdw printf("cdw\n") ; printf("zf\n"); cdw;這條語句會直接展開 還有帶參宏,#define max(a,b) ((a)+(b)) 注意的是帶參宏一定要( ) 不然有時候會引起錯誤,每一個”形參“都應該要();#define year (3652460606060 ) 安理說是可以的,但是year是int型的已經超過了範圍,是以要把它搞成無符号長整形。#define year (3652460606060ul ) 這樣才是正确的 宏定義的變量是不占記憶體空間的,直接替換減少開銷,但是變量替換是不進行類型檢查;函數的變量要占用空間、要壓棧等操作,就需要很大的開銷,但是調用函數時,編譯器會檢查函數變量的類型是否相同。内聯函數集合普通函數、宏定義的兩個優勢,它直接就地展開,直接替換,減少開銷,同時編譯器也會檢查變量的類型。但是函數體積要小,不然效率反而很低,至于 原因暫時不詳。

6.3、内聯函數:對函數就地展開,像宏定義一樣,這樣減少開銷,同時也檢查變量的類型。但是必須函數的内部體積小才用這種方式,以達到更好的效率。體積大的函數就作為普通函數。内聯函數通過在函數定義前加inline關鍵字實作。

  • 6.4、條件編譯的應用:做一個調試開關。#define DEBUG #undef DEBUG 是登出 DEBUG 宏
#ifdef DEBUG
#define debug(x) printf(x)
#else
#define debug(x)
#endif
           

6.5、函數:

(1)整個程式分成多個源檔案,一個檔案分成多個函數,一個函數分成多個語句,這就是整個程式的組織形式。這樣組織的好處在于:分化問題、便于編寫程式、便于分工。

(2)函數的出現是人(程式員和架構師)的需要,而不是機器(編譯器、CPU)的需要。

(3)函數的目的就是實作子產品化程式設計。說白了就是為了提供程式的可移植性。

<1>函數書寫的一般原則:

第一:遵循一定格式。函數的傳回類型、函數名(男女廁所)、參數清單(太多用結構體)等。

第二:一個函數隻做一件事:函數不能太長也不宜太短(一個螢幕的大小),原則是一個函數隻做一件事情。

第三:傳參不宜過多:在ARM體系下,傳參不宜超過4個。如果傳參确實需要多則考構體打包考慮。

第四:盡量少碰全局變量:函數最好用傳參傳回值來和外部交換資料,不要用全局變量。

<2> 之是以函數能被調用,根本實質是在編譯時,檢查到了該函數的聲明,不是因為函數定義了(當然也要定義才行,隻是不是本質)。

6.6、遞歸函數:自己調用自己的函數,常用舉例:階乘 int jiecheng( int n) 斐波那契數例:f(n)=f(n-1)+f(n-2) n>2的正整數

{ int he(int n)

注意:if(n<1) if(3==n||4==n)

棧溢出:遞歸函數會不停的耗費棧空間 { { 是以要注意遞歸不要太多 printf("error\n"); return 1; 收斂性:必須 要有一個終止遞歸的條件 } }

else if(n>1) else if(n>4)
{ {
return n*jiecheng(n-1); return he(n-1) +he(n-2)
} }
else
{
return 1;
}
           

6.7、函數庫:<1>靜态連結庫其實就是商業公司将自己的函數庫源代碼經過隻編譯不連接配接形成.o的目标檔案,然後用ar工具将.o檔案歸檔成.a的歸檔檔案(.a的歸檔檔案又叫靜态連結庫檔案)。

商業公司通過釋出.a庫檔案和.h頭檔案來提供靜态庫給客戶使用;客戶拿到.a和.h檔案後,通過.h頭檔案得知庫中的庫函數的原型,然後在自己的.c檔案中直接調用這些庫檔案,在連接配接的時候連結器會去.a檔案中拿出被調用的那個函數的編譯後的.o二進制代碼段連結進去形成最終的可執行程式。

<2>動态連結庫本身不将庫函數的代碼段連結入可執行程式,隻是做個 标記。然後當應用程式在記憶體中執行時,運作時環境發現它調用了一個動态庫中的庫函數時,會去加載這個動态庫到記憶體中,然後以後不管有多少個應用程式去調用這個庫中的函數都會跳轉到第一次加載的地方去執行(不會重複加載)。

也就是在運作時,會把庫函數代碼放入記憶體中,然後多個程式要用到庫函數時,就從這段記憶體去找,而靜态連結對于多程式就是重複使用庫函數,比較占記憶體。

(1) gcc中編譯連結程式預設是使用動态庫的,要想靜态連結需要顯式用-static來強制靜态連結。

(2) 庫函數的使用需要注意3點:第一,包含相應的頭檔案;第二,調用庫函數時注意函數原型;第三,有些庫函數連結時需要額外用-lxxx來指定連結;第四,如果是動态庫,要注意-L指定動态庫的位址。

6.8、常見的兩個庫函數:<1>C庫中字元串處理函數包含在string.h中,這個檔案在ubuntu系統中在/usr/include中字元串函數 如:memcpy(記憶體字元串複制,直接複制到目标空間)确定src和dst不會overlap重複,則使用memcpy效率高memmove(記憶體字元串複制,先複制到一個記憶體空間,然後再複制到目标空間)确定會overlap或者不确定但是有可能overlap,則使用memove比較保險

memset strncmp
memcmp strdup
memchr strndup
strcpy strchr
strncpy strstr
strcat strtok
strncat 。。。
strcmp
           

<2> 數學函數:math.h 需要加 -lm 就是告訴連結器到libm中去查找用到的函數。

C連結器的工作特點:因為庫函數有很多,連結器去庫函數目錄搜尋的時間比較久。為了提升速度想了一個折中的方案:連結器隻是預設的尋找幾個最常用的庫,如果是一些不常用的庫中的函數被調用,需要程式員在連結時明确給出要擴充查找的庫的名字。

連結時可以用-lxxx來訓示連結器去到libxxx.so中去查找這個函數。

6.9、自制靜态連結庫:

(1)第一步:自己制作靜态連結庫,首先使用gcc -c隻編譯不連接配接,生成.o檔案;然後使用ar工具進行打包成.a歸檔檔案庫名不能随便亂起,一般是lib+庫名稱,字尾名是.a表示是一個歸檔檔案 注意:制作出來了靜态庫之後,釋出時需要釋出.a檔案和.h檔案。

(2)第二步:使用靜态連結庫,把.a和.h都放在我引用的檔案夾下,然後在.c檔案中包含庫的.h,然後直接使用庫函數。

備注:

<1>.a 檔案,字首一定要加lib ,如 libzf.a ; 連結屬性 -l(小L),表示庫名,屬性-L表示庫的路徑。是以:gcc cdw.c -o cdw -lzf -L ./include -I(大i) ./include

<2> 頭檔案“ ”表示外部自定義,如果沒加路徑屬性,預設目前路徑找,如果在其他檔案夾下,必須用 -I(大i) 路徑。用<>表示的頭檔案一種是在編譯器下的庫檔案找,第二種是自己定義的庫檔案找,但是要定義其路徑。

<3> 在makefile檔案中用到gcc/arm-gcc 那麼在shell中就用相應的編譯器 gcc/arm-gcc .

<4> nm ./include/libmax.a 檢視max庫的資訊,有哪些 .o 檔案 .o檔案有哪些函數。

舉例:makefile: arm-gcc aston.c -o aston.o -c arm-ar -rc libaston.a aston.o

6.9.1、自制動态連結庫:

<1>動态連結庫的字尾名是.so(對應windows系統中的dll),靜态庫的擴充名是.a .

<2>第一步:建立一個動态連結庫。gcc aston.c -o aston.o -c -fPIC (-fPIC表示設定位置無關碼) gcc -o libaston.so aston.o -shared (-shared表示使用共享庫) 注意:做庫的人給用庫的人釋出庫時,釋出libxxx.so和xxx.h即可。

第二步:使用自己建立的共享庫。gcc cdw.c -o cdw -lmax.so -L ./動态連結庫 -I ./動态連結庫

第三步:上述編譯成功了,但是在 ./cdw 執行時會報錯,原因是采用動态連結,在可執行檔案隻是做了一個标記,标記是使用了哪個函數庫的哪個函數。

并沒有将庫函數加載到源檔案中,是以可執行檔案很小,在執行時,需要立即從系統裡面找到使用到的函數庫,然後加載到記憶體中,在linux系統中 預設是從 /usr/bin 中尋找,(不确定:如果使用shell中運作)會先執行環境變量的路徑然後再查找 /usr/bin;是以我們可以用兩種辦法解決運作的問題

第四步:将動态庫 libmax.so 複制到 /usr/lib 下面,但是如果以後所有的庫都這樣放的話,會越來越臃腫,導緻運作速度變慢(一個一個查找);或者是新添加一個環境變量

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/mnt/hgfs/share/include (将庫 libmax.so 複制到這個路徑下面)這樣就可以運作了。

<3>使用 ldd 指令判斷一個可執行檔案是否能運作成功;ldd cdw linux-gate.so.1 => (0xb77a8000) libmax.so => not found //發現 not found意思就是沒有找到對應的函數庫 libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75e2000) /lib/ld-linux.so.2 (0xb77a9000)

七、存儲類&作用域&生命周期&連結屬性

7.1、概念詞:存儲類(棧、堆、資料區、.bss段、.text段) 作用域(代碼塊作用範圍,也就是變量作用的範圍) 生命周期(變量的誕生和死亡) 連結屬性(外連結屬性、内連結屬性、無連接配接屬性)

7.2、Linux下的記憶體映射(配置設定情況、組織情況):見圖記憶體映射。其中有關程序的空間,如程序控制塊、頁表等都是在核心裡面的。檔案區是映射外部檔案的,如打開記事本,那麼這個檔案臨時存放在檔案區域。(見引用資料)

問題:虛拟位址技術?解決:後期在Linux應用/網絡程式設計會講。

OS下和裸機下C程式加載執行的差異?解決:在arm裸機第十六部分有介紹。

7.3、存儲類關鍵字:

<1> auto 自動的(一個用法:修飾局部變量,在定義變量時可以省略) 【外連結:與第二個c檔案連結】【内連結:隻與本c檔案連結】【無連接配接:就是無連結】

<2> static 靜态的(有兩個用法,第一個是修飾局部變量,意思是當作全局變量,存放在資料區,作用域隻是定義的那個函數範圍,生命周期和整個程式一樣,屬于無連接配接

第二個是修改全局變量/函數,意思是這個全局變量/函數隻在目前c檔案有效,其他c檔案是不能使用它的,屬于内連結,普通全局變量屬于外連接配接) <3>register 寄存器(一個用法,修飾變量,作用是讓編譯器把這個變量放在寄存器中,當這個變量頻繁的被使用時,用這個方法可以提高效率,但有時候不一定就放在寄存器,因為寄存器是有限的,沒有剩餘的寄存器了)

<4>extern (一個用法,修飾全局變量,表示該檔案要使用的這個變量,在另外一個c檔案中已經定義了,其一個聲明的作用,不能初始化)

<5>volatile (一個用法,修飾變量,表示對這個變量的語句不要去優化)

(1) volatile的字面意思:可變的、易變的。C語言中volatile用來修飾一個變量,表示這個變量可以被編譯器之外的東西改變。編譯器之内的意思是變量的值的改變是代碼的作用,編譯器之外的改變就是這個改變不是代碼造成的,或者不是目前代碼造成的,編譯器在編譯目前代碼時無法預知。譬如在中斷處理程式isr中更改了這個變量的值,譬如多線程中在别的線程更改了這個變量的值,譬如硬體自動更改了這個變量的值(一般這個變量是存在寄存器,或許當其他程序要用到這個寄存器時,就把這個寄存器的變量給改變了,同時也就改變了這個變量)

(2) 以上說的三種情況(中斷isr中引用的變量,多線程中共用的變量,硬體會更改的變量)都是編譯器在編譯時無法預知的更改,此時應用使用volatile告訴編譯器這個變量屬于這種(可變的、易變的)情況。編譯器在遇到volatile修飾的變量時就不會對改變量的通路進行優化,就不會出現錯誤。

(3) 編譯器的優化在一般情況下非常好,可以幫助提升程式效率。但是在特殊情況(volatile)下,變量會被編譯器想象之外的力量所改變,此時如果編譯器沒有意識到而去優化則就會造成優化錯誤,優化錯誤就會帶來執行時錯誤。

而且這種錯誤很難被發現。

(4) volatile是程式員意識到需要volatile然後在定義變量時加上volatile,如果你遇到了應該加volatile的情況而沒有加程式可能會被錯誤的優化。如果在不應該加volatile而加了的情況程式不會出錯隻是會降低效率。

是以我們對于volatile的态度應該是:正确區分,該加的時候加不該加的時候不加,如果不能确定該不該加為了保險起見就加上。

舉例子1:int a=3 ,b,c;b=a; c=b;

那麼編譯器會優化成 c=b=a=3; 如果此時遇到上述三種情況,突然改變了a的值,那麼,對于沒有優化前是對的,但是對于優化後,那麼c仍然是3,就會出錯。

是以當我們程式員知道這個變量會發生改變時,就應該加 volatile,提示編譯器不要幫我們做優化。

舉列子2:

int square(volatile int *ptr)
{
   return *ptr * *ptr;
}
           

這段代碼的有個惡作劇。這段代碼的目的是用來返指針ptr指向值的平方,但是,由于ptr指向一個volatile型參數,編譯器将産生類似下面的代碼:

int square(volatile int *ptr)
{
   int a,b;
   a = *ptr;
   b = *ptr;
   return a * b;
}

           

由于*ptr的值可能被意想不到地該變,是以a和b可能是不同的。結果,這段代碼可能返不是你所期望的平方值!正确的代碼如下:

long square(volatile int *ptr)
{
  int a;
   a = *ptr;
  return a * a; 
}
           

<6> restrict (1)c99中才支援的,是以很多延續c89的編譯器是不支援restrict關鍵字,gcc支援的。

(2)restrict 作用是讓編譯器對這個變量做一些優化,讓它提高效率。下面的網站有列子。

(3)restrict隻用來修飾指針,不能修飾普通變量,它表示隻能該指針才能修改它的内容。如用memcpy時,兩個記憶體存在重疊的現象。

(4)(這個網站裡面有詳細的例子)

(5)memcpy和memmove的差別 void *memcpy( void * restrict dest ,const void * restrict src,sizi_t n);這樣它可以優化成memmove原理的方式(當存在重疊時,先複制到一段記憶體空間,然後再把它複制到目的空間)

7.4、作用域:

(1)全局變量起名字一般是 g_a;

(2)名字加字首表示

7.5、總結:<1>局部變量位址由運作時在棧上配置設定得到,多次執行時位址不一定相同,函數不能傳回該類變量的位址(指針)作為傳回值。

<2>為什麼要分為資料段和.bbs段?因為當加載到記憶體重定位時,如果這些資料(包括0)一個一個的複制,會降低效率,為0的變量,直接清記憶體就可以了,這樣提高效率。

<3>在頭檔案聲明全局變量時, extern int a; 聲明函數就是 void int max(int a, int b);

<4>寫程式盡量避免使用全局變量,尤其是非static類型的全局變量。能确定不會被其他檔案引用的全局變量一定要static修飾。(因為全局變量占記憶體的時間是最長的,要看你的變量是不是需要這麼長的時間,這樣可以節約記憶體空)

八、一些雜散但值得讨論的問題

8.1、作業系統的了解:

<1>它是一個管理階級者,管理所有資源,負責調配優化等操作。這樣想象,就像裸機一樣的話,要實作LED閃爍的程序、序列槽傳輸的程序、蜂鳴器等這些,他們都要搶占一些資源,這個時候沒有作業系統,就亂成一鍋粥,當有了OS的時候,它就專門負責資源的調配,讓各個任務都能很好的實施,起一個決策者的作用。

<2>如果我們要做一個産品,軟體系統到底應該是裸機還是基于作業系統呢?本質上取決于産品本身的複雜度。隻有極簡單的功能、使用極簡單的CPU(譬如單片機)的産品才會選擇用裸機開發;一般的複雜性産品都會選擇基于作業系統來開發。

<3>作業系統負責管理和資源調配,應用程式負責具體的直接勞動,他們之間的接口就是API函數。當應用程式需要使用系統資源(譬如記憶體、譬如CPU、譬如硬體操作)時就通過API向作業系統發出申請,然後作業系統響應申請幫助應用程式執行功能。

8.2、C庫函數和API的關系:

<1>從核心的角度看,需要考慮提供哪些有用的API函數,并不需要關注它們如何被使用。故程式設計時用API函數會感覺到不好用,沒有做優化。系統隻負責做出一個可以用的API,沒有考慮到使用者使用的友善性。是以c庫函數對API做了一些優化,讓使用者使用庫函數更容易達到我們想要的目的。

<2>庫函數實質還是用的API,或者調用了一個API,也或者調用了更多的API,隻不過是做了更多的優化。比如 庫函數fopen ,而API是open.

<3>有的庫函數沒有用到API,比如strcpy函數(複制字元串)和atoi函數(轉換ASCII為整數),因為它們并不需要向核心請求任何服務。

8.3、不同平台(windows、linux、裸機)下庫函數的差異

(1)不同作業系統API是不同的,但是都能完成所有的任務,隻是完成一個任務所調用的API不同。

(2)庫函數在不同作業系統下也不同,但是相似性要更高一些。這是人為的,因為人下意識想要屏蔽不同作業系統的差異,是以在封裝API成庫函數的時候,盡量使用了同一套接口,是以封裝出來的庫函數挺像的。

但是還是有差異,是以在一個作業系統上寫的應用程式不可能直接在另一個作業系統上面編譯運作。于是乎就有個可移植性出來了。

(3)跨作業系統可移植平台,譬如QT、譬如Java語言。

8.4、

<1>main()函數的寫法:int main(int argc,char **argv) ; int main(int argc ,char *argv[ ] ); 這兩種寫法是一樣的。二重指針等同于指針數組。

<2>不管是主函數還是功能函數,它都應該有一個傳回值,而主函數的傳回值是給調用的那個人的/main函數從某種角度來講代表了我目前這個程式,或者說代表了整個程式。main函數的開始意味着整個程式開始執行, main函數的結束傳回意味着整個程式的結束。誰執行了這個程式,誰就調用了main。誰執行了程式?或者說程式有哪幾種被調用執行的方法?一個程式當然會運作,那麼就是調用了main( ).

<3>inux下一個新程式執行的本質

(1)表面來看,linux中在指令行中去./xx執行一個可執行程式

(2)我們還可以通過shell腳本來調用執行一個程式

(3)我們還可以在程式中去調用執行一個程式(fork exec)

總結:我們有多種方法都可以執行一個程式,但是本質上是相同的。linux中一個新程式的執行本質上是一個程序的建立、加載、運作、消亡。linux中執行一個程式其實就是建立一個新程序然後把這個程式丢進這個程序中去執行直到結束。

新程序是被誰開啟?在linux中程序都是被它的父程序fork出來的。

分析:指令行本身就是一個程序,在指令行底下去./xx執行一個程式,其實這個新程式是作為指令行程序的一個字程序去執行的。

總之一句話:一個程式被它的父程序所調用。

結論:main函數傳回給調用這個函數的父程序。父程序要這個傳回值幹嘛?父程序調用子程序來執行一個任務,然後字程序執行完後通過main函數的傳回值傳回給父程序一個答複。這個答複一般是表示子程序的任務執行結果完成了還是錯誤了。

(0表示執行成功,負數表示失敗,正規的要求傳回失敗的原因,傳回-1表示什麼,傳回-2又表示什麼,然後父程序好做相應的處理)

(4) main函數的傳回值應當是int 型。父程序要求是int型的,如果寫成 float 型,則傳回就為0,這樣是要出錯的。

8.5 用shell腳本來看main()的傳回值。如:#!/bin/sh 其文本格式為 .sh

./a.out echo $?

8.6、argc、argv與main函數的傳參:當我們的父程序不需要傳參時,就用 int main(void);當我們需要傳參時,就應該是 int main(int argv ,char *argc[ ]);它預設本身就是一個參數,占了argv[0]這個位置,它裡面存的是 ./a.out (這個相應變化)

如:./a.out boy girl ;則 argv=3; argc[0]="./a.out"; argc[1]="boy"; argc[2]="girl" ; printf("%s\n",argc[0]);

解釋:argv表示傳了多少個參數,argc實質是存的一個指針,也就是一個位址,隻是沒有一個被綁定的變量名而已。這個位址指向一個字元串,一般字元串都和指針相關。是以可以稱之為字元串數組,每一個都存了一個字元串。

在程式内部如果要使用argc,那麼一定要先檢驗argv,先檢驗個數,然後使用。

8.7、void類型的本質:即使空型又是未知類型,看具體情況。比如一個函數void表示不傳回, void *malloc(20);就是未知類型。

(1)程式設計語言分2種:強類型語言和弱類型語言。強類型語言中所有的變量都有自己固定的類型,這個類型有固定的記憶體占用,有固定的解析方法;弱類型語言中沒有類型的概念,所有變量全都是一個類型(一般都是字元串的),程式在用的時候再根據需要來處理變量。就如:makefile、html語言。

(2)C語言就是典型的強類型語言,C語言中所有的變量都有明确的類型。因為C語言中的一個變量都要對應記憶體中的一段記憶體,編譯器需要這個變量的類型來确定這個變量占用記憶體的位元組數和這一段記憶體的解析方法。

(3)void類型的正确的含義是:不知道類型,不确定類型,還沒确定類型、未知類型,但是将來一定有類型。

(4)void *a;(編譯器可以通過)定義了一個void類型的變量,含義就是說a是一個指針,而且a肯定有确定的類型,隻是目前我還不知道a的類型,還不确定,是以标記為void。

void “修飾”的是指針,因為指針就是記憶體位址,它不知道指向的另一個變量是哪一種類型,而變量一定是确定的,void a;就是錯誤的。

8.9、C語言中的NULL

NULL在C/C++中的标準定義

(1)NULL不是C語言關鍵字,本質上是一個宏定義,其保護指針的作用,不要讓他亂開槍。

(2)NULL的标準定義:

#ifdef _cplusplus // 條件編譯c++環境 #define NULL 0 #else #define NULL (void *)0 // 這裡對應C語言的情況 #endif

解釋:C++的編譯環境中,編譯器預先定義了一個宏_cplusplus,程式中可以用條件編譯來判斷目前的編譯環境是C++的還是C的。

NULL的本質解析:NULL的本質是0,但是這個0不是當一個數字解析,而是當一個記憶體位址來解析的,這個0其實是0x00000000,代表記憶體的0位址。(void *)0這個整體表達式表示一個指針,這個指針變量本身占4位元組,位址在哪裡取決于指針變量本身,但是這個指針變量的值是0,也就是說這個指針變量指向0位址(實際是0位址開始的一段記憶體)。如 char *p=NULL; 因為0位址本身就不是我們來通路的,是以 *p時是不可通路的。在程式運作的邏輯上就不會出錯。

正規寫:

int *p = NULL;// 定義p時立即初始化為NULL
p = xx;
if (NULL != p)
{
*p // 在确認p不等于NULL的情況下才去解引用p
}
           

(1)'\0'是一個轉義字元,他對應的ASCII編碼值是0,記憶體值是0,一個char空間。

(2)'0'是一個字元,他對應的ASCII編碼值是48,記憶體值是int型48,一個char空間。

(3)0是一個數字,沒有ASCll編碼, 記憶體值是int型0,一個int空間。

(4)NULL是一個表達式,是強制類型轉換為void *類型的0,記憶體值是0(記憶體位址),一個int空間。

8.9.1、運算中的臨時匿名變量

<1>“小動作”:進階語言在運算中允許我們大跨度的運算。意思就是低級語言中需要好幾步才能完成的一個運算,在進階語言中隻要一步即可完成。譬如C語言中一個變量i要加1,在C中隻需要i++即可,看起來隻有一句代碼。但實際上翻譯到彙編階段需要3步才能完成:第1步從記憶體中讀取i到寄存器,

第2步對寄存器中的i進行加1,第3步将加1後的i寫回記憶體中的i。

<2> float a=12.3; int b=(int)a; (int )a 就是匿名變量;先找一個記憶體空間,裡面存(int)a;然後把這個值指派給b;最後匿名值銷毀。

float a; int b=10; a=b/3; 左邊是3.00000;右邊是3;其中有個匿名變量,先找一個記憶體空間,裡面存 b/3; 然後把它再轉換成float型,再指派個a;最後匿名值銷毀。

8.9.2 分析DEBUG宏

學習級:

#define DEBUG #undef DEBUG 是登出 DEBUG 宏
#ifdef DEBUG
#define debug(x) printf(x)
#else
#define debug(x)
#endif
           

應用級:

#ifdef DEBUG
#define DBG(...) fprintf(stderr, " DBG(%s, %s( ), %d): ", __FILE__, __FUNCTION__, __LINE__); fprintf(stderr, __VA_ARGS__)
#else
#define DBG(...)
#endif
           

解 釋:<1>...表示變參,提示編譯器不要對參數個數斤斤計較,不要報錯;其實完全可以把 ...換成 cdw 也是可以的,隻是非要裝一下而已。

<2> FILE 和 _FUNCTION_和 LINE 都是c庫函數的宏定義,分别表示要輸出的這句話屬于哪個檔案名、屬于哪個函數名、在第幾行。

<3> 在 fprintf(stderr,"cdw");其中stderr是c庫函數中宏定義了的,這是VC6.0找到的 #define stderr (&_iob[2]) ;也就是說stderr是一個被宏定義了的指針,它是标準錯誤輸出流對象(stderr),輸出到螢幕上。

fprintf是C/C++中的一個格式化寫—庫函數,位于頭檔案中,其作用是格式化輸出到一個流/檔案中;(重點是流/檔案)

printf()函數是格式化輸出函數, 一般用于向标準輸出裝置按規定格式輸出(重點是标準輸出裝置,有時候輸出的不一定顯示在螢幕上,隻是編譯器規定顯示到螢幕上而已。)

總結:也就是說printf()其實不是輸出螢幕上的,隻是這個标準輸出裝置中,編譯器規定顯示到螢幕上而已,而真正輸出到螢幕是fprintf(stderr,"cdw");其中stderr就是輸出到螢幕上的流。它也可以 fprintf( FILE *stream, const char *format,...),這個就是輸出到檔案流中的。

比如:一般情況下,你這兩個語句運作的結果是相同的,沒有差別,隻有一下情況才有差別:運作你的程式的時候,指令行上把輸出結果進行的轉向,比如使用下面的指令把你的程式a.c運作的結果轉向到記事本檔案a.txt:a.exe > a.txt

在這樣的情況,如果使用printf輸出錯誤資訊,會儲存到a.txt檔案裡面,如果使用fprintf輸出錯誤,會顯示在螢幕上。

<4>上面中的__VA_ARGS__也是一個宏定義,表示預處理時實際的參數。如:DBG("tiaoshi.\n");

則允許的效果是 DBG(debug.c, main( ), 14): tiaoshi.

核心級:

#ifdef DEBUG_S3C_MEM#define DEBUG(fmt, args...)printk(fmt, ##args)#else#define DEBUG(fmt, args...)do {} while (0)#endif
           

九、連結清單&狀态機與多線程(9.9.1?具體連結清單實作留到驅動子產品講解)

9.1、連結清單是一個一個的節點,每一個節點分為兩部分,一部分是資料區(可以由多個類型的資料),另一部分是指向下一個節點的指針;結構體定義裡面的變量并沒有生成,是不占空間的,相當于聲明的作用。

9.2、連結清單的資料存放在記憶體的那個空間呢?(棧,不靈活,不能用date資料段)是以隻能用堆記憶體,申請一個節點的大小并檢測NULL, 要使用它,就得清理它,因為上一個程序用了這段記憶體,存的是髒資料, 然後對這個節點記憶體指派,連結起來.

9.3、當要改變頭節點是,也就是要給head=p指派時,必須傳 head位址即 形參(struct student *head);這樣才能真正改變,不然傳一個 (struct student head)隻是單純的指派。

9.4、在scanf("%d",&(s->age)) 一定要注意,studeny *s; s->age通路的是一個變量,而不要了解成位址,是以要加&,scanf要注意&;

9.5、細節:<1>在 .h檔案中聲明一個函數要用分号,而且是英文符号.用無頭節點的方式,需要修改頭指針位置,是以比較複雜。

<2> 定義一個node *head=NULL,想要改變head值通過函數傳參是不行的,因為head是一個位址,傳參過去,隻是指派給另一個指針而已,隻能修改它指向的資料,而本身(位址)是不能修改的,是以要先傳回修改好的位址,然後再head=node_add(head)

<3>定義、用指針都應該想到NULL,如 node *head=NULL; node *new=(node *)mallo(sizeof(node));if(NULL!=new){ }

<4>在結構體想定義一個字元串時不要用 char *name; 應該要用char name[10];如果使用第一種的話,編譯通過,執行錯誤,因為為name指派時就要放在代碼段中,而代碼段已确定了,是以報段錯誤。

9.6、頭節點、頭指針、第一個節點:頭節點是一個節點,頭節點的下一個指向第一個節點,頭節點的資料一般存的是連結清單長度等資訊,也可以是空,頭指針指向頭節點。連結清單可以沒有頭節點,但不能沒有頭指針。

頭節點可以想成數組的0位置,其餘節點當作從1開始,是以有頭節點的長度可以定義為就是含有真實資料節點的個數。

9.7、删除一個節點應該做的事:如果這個節點的資料不重要,一定要記住free()掉,你邏輯上删除,其實仍然存在記憶體中的,頭節點的好處就是函數傳回值int可以幫助我們一些資訊,而沒有頭節點有時必須傳回head;

9.8、單連結清單之逆序:見代碼。

9.9、單連結清單的優點和缺點:<優點>單連結清單是對數組的一個擴充,解決了數組的大小比較死闆不容易擴充的問題。使用堆記憶體來存儲資料,将資料分散到各個節點之間,其各個節點在記憶體中可以不相連,節點之間通過指針進行單向連結。連結清單中的各個節點記憶體不相連,有利于利用碎片化的記憶體。

<缺點>單連結清單各個節點之間隻由一個指針單向連結,這樣實作有一些局限性。局限性主要展現在單連結清單隻能經由指針單向移動(一旦指針移動過某個節點就無法再回來,如果要再次操作這個節點除非從頭指針開始再次周遊一次),是以單連結清單的某些操作就比較麻煩(算法比較有局限)。

回憶之前單連結清單的所有操作(插入、删除節點、 周遊、從單連結清單中取某個節點的數·····),因為單連結清單的單向移動性導緻了不少麻煩。

總結:單連結清單的單向移動性導緻我們在操作單連結清單時,目前節點隻能向後移動不能向前移動,是以不自由,不利于解決更複雜的算法。

9.9.1、 核心連結清單的思想是:<1>先做一個純連結清單,沒有資料區,隻有節點的連結方法。然後要做一個連結清單出來,直接用純連結清單然後稍加修改就可以了。

<2>核心中__的方法不要輕易使用,是給核心用的,否則容易出錯,使用者應該使用沒有__的方法;如:__list_add() ; list_add();

<3>核心預設是頭指針+頭節點的思路。

<4>其實質就是操作裡面内嵌 純連結清單這個變量,再利用controf宏來通路結構體的資料。詳情見驅動。

9.9.2、狀态機:

<1>概念:其實就是有多種狀态切換,如電腦的休眠、關機、睡眠。

<2>類型:(1)Moore型狀态機特點是:輸出隻與目前狀态有關(與輸入信号無關)。相對簡單,考慮狀态機的下一個狀态時隻需要考慮它的目前狀态就行了。

(2)Mealy型狀态機的特點是:輸出不隻和目前狀态有關,還與輸入信号有關。狀态機接收到一個輸入信号需要跳轉到下一個狀态時,狀态機綜合考慮2個條件(目前狀态、輸入值)後才決定跳轉到哪個狀态。

<3>了解:要時時刻刻檢查目前狀态,用循環+switch(狀态);然後根據輸入信号,進行更多的處理,轉換到其他狀态。

十、增補知識

10.1、一個位元組可以表示8位字元,字元真的有256種,128~255表示西歐字元,是不常見,詳情見文檔。字元相加的時候,會自動轉成 int型加。

10.2、在C中,預設的基礎資料類型均為signed,現在我們以char為例,說明(signed) char與unsigned char之間的差別。

首先在記憶體中,char 與 unsigned char 沒有什麼不同,都是一個位元組,唯一的差別是,char 的最高位為符号位,是以 char 能表示 -127~127,unsigned char 沒有符号位,是以能表示 0~255,這個好了解,8 個 bit,最多 256 種情況,是以無論如何都能表示 256 個數字。

10.3、為什麼在連結時需要一個連結位址?因為資料是要放在一個模拟位址記憶體空間的,它要把這個資料先加載到寄存器,才能給 cpu 使用,那麼寄存器怎麼知道是哪個記憶體位址位置呢,是因為在編譯時,編譯出像 ldr r0 0x12345678 ,而這個 0x12345678 就是記憶體位址,再編譯出像 ldr r1,[r0] ,這樣就可以拿到0x12345678記憶體位置的資料了

10.4、printf 變參?

繼續閱讀