C語言高級篇
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字節對齊):http://blog.csdn.net/hbuxiaofei/article/details/9491953
1.4、char/int/short/long/float/double型:放在內存的長度和解析作用。(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。(記住:不要把申請的地址給搞丟了, 不然自己用不了,也釋放不了)
申請一段內存,可以是: malloc(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、 結構體做為形參時,應盡量用指針/地址方式來傳,因為結構體變量有時會占很大,效率很低。
4.0、 int *p=&u; p存放的是變量u的地址,而&p的意思就是變量p本身的地址。
4.1、當要傳參的個數比較多時,我們可以打包成一個結構體,傳參的個數越多,其開銷就更大.
4.2 一個函數作用其實就是輸入輸出,參數可以作為輸入,返回可以作為輸出,但是當要返回多個輸出時,這時候就不夠用了,所以常常返回值用來判斷程序又沒有出錯,而參數
就是當作輸入輸出的,輸入時可以加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'是換行,前者使光標到行首,後者使光標下移一格,通常敲一個回車鍵,即是回車,又是換行(\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 *intType; 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.6?)
{
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字節一跳。就相當於加了+4*6;
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.........
也可以自己賦值,但是每一個值是不一樣的,否則邏輯上出錯。
enum 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的匯編代碼。編譯器聚焦核心功能後,就剝離出了一些非核心的功能到預處理器去了。
(2)預處理器幫編譯器做一些編譯前的雜事。如:(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 (365*24*60*60*60*60 ) 安理說是可以的,但是year是int型的已經超過了范圍,所以要把它搞成無符號長整形。
#define year (365*24*60*60*60*60ul ) 這樣才是正確的
宏定義的變量是不占內存空間的,直接替換減少開銷,但是變量替換是不進行類型檢查;
函數的變量要占用空間、要壓棧等操作,就需要很大的開銷,但是調用函數時,編譯器會檢查函數變量的類型是否相同。
內聯函數集合普通函數、宏定義的兩個優勢,它直接就地展開,直接替換,減少開銷,同時編譯器也會檢查變量的類型。但是函數體積要小,不然效率反而很低,至於
原因暫時不詳。
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.2 ?虛擬地址技術?)
{
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;
}
舉例子更多:http://www.jb51.net/article/37489.htm
<6> restrict (1)c99中才支持的,所以很多延續c89的編譯器是不支持restrict關鍵字,gcc支持的。
(2)restrict 作用是讓編譯器對這個變量做一些優化,讓它提高效率。下面的網站有列子。
(3)restrict只用來修飾指針,不能修飾普通變量,它表示只能該指針才能修改它的內容。如用memcpy時,兩個內存存在重疊的現象。
(4)http://blog.chinaunix.net/uid-22197900-id-359209.html (這個網站裡面有詳細的例子)
(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文件中聲明一個函數要用分號,而且是英文符號.用無頭節點的方式,需要修改頭指針位置,所以比較復雜,詳情:http://blog.csdn.net/leo115/article/details/8602621
<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、頭節點、頭指針、第一個節點:頭節點是一個節點,頭節點的下一個指向第一個節點,頭節點的數據一般存的是鏈表長度等信息,也可以是空,頭指針指向頭節點。鏈表可以沒有頭節點,但不能沒有頭指針。
所以以後看鏈表要看有沒有頭節點 地址:(http://blog.csdn.net/zhenyusoso/article/details/6092843)
頭節點可以想成數組的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 變參?
10.5、arm-2009q3.tar.bz2 這套編譯器自帶了函數庫,比如有strcmp , malloc ,printf 等,但是有些庫函數我們卻不能用他們,比如printf,因為這個函數默認是同過屏幕輸出的,而我們常用uart調試。感覺malloc也不能用,因為我們不知道內存哪一塊做了堆內存,只有系統才知道。
10.6、清bss段:編譯器可能已經幫我們做了,只是在重定位那節,因為要重定位那部分內存空間並沒有清0 ,所以要手動編程清bss段。