一、內(nèi)存大話題
1.0、內(nèi)存就是程序的立足之地,體現(xiàn)內(nèi)存重要性。
1.1、內(nèi)存理解:
內(nèi)存物理看是有很多個(gè)Bank(就是行列陣式的存儲(chǔ)芯片),每一個(gè)Bank的列就是位寬 ,每一行就是Words,則存儲(chǔ)單元數(shù)量=行數(shù)(words)×列數(shù)(位寬)×Bank的數(shù)量;通常也用M×W的方式來表示芯片的容量(或者說是芯片的規(guī)格/組織結(jié)構(gòu))。
M是以位寬為單位的總?cè)萘浚瑔挝皇钦?,W代表位寬, 單位是bit。計(jì)算出來的芯片容量也是以bit為單位,但用戶可以采用除以8的方法換算為字節(jié)(Byte)。比如8M×8,這是一個(gè)8bit位寬芯片,有8M個(gè)存儲(chǔ)單元,總?cè)萘渴?4Mbit(8MB)。1.2、c語言中其實(shí)沒有bool類型:以0表示假,非0表示真,則在內(nèi)存存儲(chǔ)是以int型存放的。如果想要表示真假,可以用int/char型做替換,在c++中就有bool x=true/false;1.3、內(nèi)存對(duì)齊:內(nèi)存對(duì)齊(提高訪問效率速度,編譯器一般默認(rèn)是4字節(jié)對(duì)齊)
1.4、char/int/short/long/float/double型:放在內(nèi)存的長度和解析作用。(int *)0,使0地址指向一個(gè)int型。又比如0000111010101可以解析成int型也可以解析成float型。1.5、Linux內(nèi)核是面向?qū)ο蟮模鴆語言是面向過程的,但可以用結(jié)構(gòu)體內(nèi)嵌指針變成面向?qū)ο蟆H?/p>
struct student{int age; //變量int lenth; //將相當(dāng)于一個(gè)類,有變量有函數(shù)char *name;void (*eat)(void); //函數(shù)指針}
1.6、棧的理解:
(1) 運(yùn)行時(shí)自動(dòng)分配&自動(dòng)回收:棧是自動(dòng)管理的,程序員不需要手工干預(yù)。方便簡(jiǎn)單。(表現(xiàn)在匯編代碼,編譯時(shí),會(huì)自動(dòng)編譯成匯編碼實(shí)現(xiàn)函數(shù)調(diào)用完立即改變棧頂)
(2) 反復(fù)使用:棧內(nèi)存在程序中其實(shí)就是那一塊空間,程序反復(fù)使用這一塊空間。(硬件上有個(gè)寄存器,用來存放棧的棧頂?shù)刂罚瑮J怯写笮〉目臻g)
(3) 臟內(nèi)存:棧內(nèi)存由于反復(fù)使用,每次使用后程序不會(huì)去清理,因此分配到時(shí)保留原來的值。
(4) 臨時(shí)性:(函數(shù)不能返回棧變量的指針,因?yàn)檫@個(gè)空間是臨時(shí)的)
(5) 棧會(huì)溢出:因?yàn)?a href="http://m.xsypw.cn/v/tag/527/" target="_blank">操作系統(tǒng)事先給定了棧的大小,如果在函數(shù)中無窮盡的分配棧內(nèi)存總能用完。棧的操作(怎么出棧怎么入棧)是由具體硬件來干預(yù),程序員只要明白原理就可以了,但是要給相應(yīng)的棧寄存器賦值。當(dāng)調(diào)用函數(shù)時(shí),變量會(huì)自動(dòng)放在棧中(入棧)當(dāng)函數(shù)調(diào)用完后,棧會(huì)自動(dòng)出棧.
( 6 ) 棧的 "發(fā)展"有四種情況,滿增棧,滿減棧,空增棧,空減棧,至于是那種要根據(jù)編譯器決定,而s5pv21 是滿減棧。1.7、堆的理解:
(1)操作系統(tǒng)堆管理器管理:堆管理器是操作系統(tǒng)的一個(gè)模塊,堆管理內(nèi)存分配靈活,按需分配。
(2)大塊內(nèi)存:堆內(nèi)存管理者總量很大的操作系統(tǒng)內(nèi)存塊,各進(jìn)程可以按需申請(qǐng)使用,使用完釋放。
(3)臟內(nèi)存:堆內(nèi)存也是反復(fù)使用的,而且使用者用完釋放前不會(huì)清除,因此也是臟的。
(4)臨時(shí)性:堆內(nèi)存只在malloc和free之間屬于我這個(gè)進(jìn)程,而可以訪問。在malloc之前和free之后都不能再訪問,否則會(huì)有不可預(yù)料的后果。
(5)程序手動(dòng)申請(qǐng)&釋放:手工意思是需要寫代碼去申請(qǐng)malloc和釋放free。(記住:不要把申請(qǐng)的地址給搞丟了, 不然自己用不了,也釋放不了)
申請(qǐng)一段內(nèi)存,可以是:
malloc(10*sizeof ( int ) );
原型:
void*malloc(size_tsize);
//指針函數(shù) size_t是宏定義int 都是便于可移植性 ,返回一個(gè)內(nèi)存地址,void *可以看出,希望申請(qǐng)的內(nèi)存用來存放什么就強(qiáng)制類型什么。
calloc( 10,sizeof ( int ) ); 原型:void *calloc(size_t nmemb, size_t size);// nmemb個(gè)單元,每個(gè)單元size字節(jié)void *realloc(void *ptr, size_t size);// 改變?cè)瓉砩暾?qǐng)的空間的大小的ptr是原來申請(qǐng)內(nèi)存的指針,size是想要重新申請(qǐng)內(nèi)存的大小使用就是*(p+1)=12 ; *(P+3)=110;
申請(qǐng)失敗返回NULL,申請(qǐng)成功返回一個(gè)地址,申請(qǐng)之后一定要檢驗(yàn)(NULL!=p)用完一定要 free ( p ) ;釋放后不是不能用,是不應(yīng)該使用了。可以給它“洗盤子‘,p=NULL;
其實(shí)申請(qǐng)的內(nèi)存并不能真正改變大小,原理是先重新申請(qǐng)一段內(nèi)存,然后把原來申請(qǐng)的內(nèi)存上的內(nèi)容復(fù)制到新的內(nèi)存上,然后釋放掉原來的內(nèi)存,返回新的指針。
(6) 在申請(qǐng)內(nèi)存時(shí),malloc(0)其實(shí)也是成功的,因?yàn)橄到y(tǒng)規(guī)定少于一定數(shù)目的大小,都申請(qǐng)規(guī)定的大小,如在win32系統(tǒng)下申請(qǐng)少于32字節(jié)的地址,最后申請(qǐng)到的空間是32字節(jié),在朱老師視頻中申請(qǐng)少于16字節(jié)的地址,最后申請(qǐng)到的是16字節(jié),至于規(guī)定多少字節(jié),由具體的系統(tǒng)而言。1.8、內(nèi)存里的數(shù)據(jù):
(1)代碼段:存放代碼二進(jìn)制、常量(char *p="linux",則”linux“存放在代碼段,是不可更改的)
(2) 數(shù)據(jù)段: 存放非0全局變量、靜態(tài)局部變量(局部只屬于函數(shù)的,不是整個(gè)程序的)
(3) bss : 存放為0的全局變量/為0的靜態(tài)局部變量、存放未初始化全局變量/靜態(tài)局部變量
注意:const int a=9; 有兩種存放方式:第一種確實(shí)存放在代碼段,讓a不能修改,第二種是仍然存放在數(shù)據(jù)段中,讓編譯器來判斷,如果有改變的代碼就會(huì)報(bào)錯(cuò)。至于那種,是不確定的,像單片機(jī)就屬于第一種。1.9、《1》一個(gè)源文件實(shí)際上是以段為單位編譯成連接成可執(zhí)行文件(a .out );這個(gè)可執(zhí)行文件總的說是分為數(shù)據(jù)段,代碼段,自定義段,數(shù)據(jù)段還可以細(xì)分成 .bbs 段。而雜段會(huì)在執(zhí)行的時(shí)候拿掉。所以a.out分為雜段,數(shù)據(jù)段(存放的是非0全局變量).bbs段,代碼段。
《2》內(nèi)存實(shí)際上被劃分了兩大區(qū)域,一個(gè)是系統(tǒng)區(qū)域,另一個(gè)是用戶區(qū)域,而每一個(gè)區(qū)域又被劃分成了幾個(gè)小區(qū)域,有堆,棧,代碼區(qū),.bbs區(qū),數(shù)據(jù)區(qū)(存放的是非0全局變量)。
《3》對(duì)于有操作系統(tǒng)而言, 當(dāng)我們?cè)趫?zhí)行a.out可執(zhí)行文件時(shí),執(zhí)行這個(gè)文件的那套程序會(huì)幫我們把雜段清掉,然后把相應(yīng)的段加載到內(nèi)存對(duì)應(yīng)的段。對(duì)于裸機(jī)程序而言,我們是使用一套工具將a.elf的可執(zhí)行程序給清掉了所有段的符號(hào)信息,把純凈的二進(jìn)制做成.bin格式的燒錄文件。所以我們加載到內(nèi)存的程序是連續(xù)的,也就是說代碼段和數(shù)據(jù)段、.bbs段都是連續(xù)的。當(dāng)然,棧空間是我們自己設(shè)置的。而且在裸機(jī)中我們不能使用malloc函數(shù),因?yàn)槲覀兪褂玫闹皇蔷幾g器、連接器工具沒有集成庫函數(shù),沒有定義堆空間區(qū)。
《4》大總結(jié)多程序運(yùn)行情況:在Linux系統(tǒng)中運(yùn)行cdw1.out時(shí),運(yùn)行這個(gè)文件的那套程序會(huì)幫我們把相應(yīng)的段加載到內(nèi)存對(duì)應(yīng)的段。然后操作系統(tǒng)會(huì)把下載到內(nèi)存的具體物理地址與每條命令(32位)的鏈接地址映射到TTB中(一段內(nèi)存空間),當(dāng)我們又運(yùn)行cdw2.out時(shí),同樣也像cdw1.out一樣加載進(jìn)去,并映射到TTB表中。而且這兩個(gè).out文件默認(rèn)都是鏈接0地址(邏輯),當(dāng)cpu發(fā)出一個(gè)虛擬地址(Linux中程序邏輯地址)通過TTB查找的物理地址是不一樣的。所以對(duì)于每一個(gè)程序而言,它獨(dú)占4G的內(nèi)存空間,看不到其他程序。
二、位操作
2.1 ~(0u)是全1;2.2 位與& 位或 | 位取反~ 位異或^2.3、位與、位或、位異或的特點(diǎn)總結(jié):
位與:(任何數(shù),其實(shí)就是1或者0)與1位與無變化,與0位與變成0位或:(任何數(shù),其實(shí)就是1或者0)與1位或變成1,與0位或無變化位異或:(任何數(shù),其實(shí)就是1或者0)與1位異或會(huì)取反,與0位異或無變化2.4、左移位<< 與右移位>> C語言的移位要取決于數(shù)據(jù)類型。對(duì)于無符號(hào)數(shù),左移時(shí)右側(cè)補(bǔ)0(相當(dāng)于邏輯移位)對(duì)于無符號(hào)數(shù),右移時(shí)左側(cè)補(bǔ)0(相當(dāng)于邏輯移位)對(duì)于有符號(hào)數(shù),左移時(shí)右側(cè)補(bǔ)0(叫算術(shù)移位,相當(dāng)于邏輯移位)對(duì)于有符號(hào)數(shù),右移時(shí)左側(cè)補(bǔ)符號(hào)位(如果正數(shù)就補(bǔ)0,負(fù)數(shù)就補(bǔ)1,叫算術(shù)移位)2.5、小記:常與 1 拿來 做位運(yùn)算。讓他取反、移位 得到想要的數(shù)。2.6、直接用宏來置位、復(fù)位(最右邊為第1位)。置位置1,復(fù)位置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表示輸出一個(gè)指針,就是指針變量(其存放的那個(gè)地址),可以理解為輸出一個(gè)地址。3.2 int* p1, p2 ; 等同于 int *p1; int p2;int*p="Linux",其不能改變*P,因?yàn)椤保欤椋睿酰⑹且粋€(gè)常數(shù)。3.3 ( 代碼規(guī)范性 )在定義指針時(shí),同時(shí)賦值為NULL,在用指針時(shí),先判斷它是不是NULL。尤其是在malloc申請(qǐng)內(nèi)存后,free(p);則一定要讓p=NULL3.4 C/C++中對(duì)NULL的理解:{ #ifdef _cplusplus// 定義這個(gè)符號(hào)就表示當(dāng)前是C++環(huán)境
#define NULL 0;// 在C++中NULL就是0#else#define NULL (void *) 0;// 在C中NULL是強(qiáng)制類型轉(zhuǎn)換為void *的0#endif
3.5、修飾詞:const (修飾變量為常量,應(yīng)該理解為不應(yīng)該去變它,當(dāng)作常量,而并非永遠(yuǎn)不能改變,當(dāng)然要看具體運(yùn)行環(huán)境,在gcc,const 這種就可以采用指針方式修改,但是在在VC6.6++中就不可以修改):其雖然是當(dāng)作常數(shù),但是仍然存放在數(shù)據(jù)段中,用指針仍然可以改變值。
第一種:const int *p;第二種:int const *p;第三種:int * const p;第四種:const int * const p;
3.6、 數(shù)組 int a[2]; 其中a是指首元素的首地址,&a是整個(gè)數(shù)組的收地址(數(shù)組指針,其這個(gè)指針指向一個(gè)數(shù)組),他們的值是一樣的,但意義不一樣,可以參照 int a; int *p=&a; 來理解。數(shù)組和指針天生姻緣在于數(shù)組名;
int a[3]; int* p=a;是可以的,但是 int *p=&a;就會(huì)報(bào)錯(cuò),盡管他們的值是一樣的,但意義不一樣,所以是不允許的,除非強(qiáng)制類型轉(zhuǎn)換。在訪問時(shí)是a[0],其實(shí)編譯器會(huì)把它變成*(a+0)的方式,只是用a[0]看起來更方便,封裝了一下而已,實(shí)質(zhì)還是指針。3.7、 siziof()是一個(gè)運(yùn)算符,測(cè)試所占內(nèi)存空間,如 int a[100] ;sizeof(a)=400;
與strlen( )要有所區(qū)別,他是測(cè)字符串實(shí)際長度的,不包括‘\0‘,如果給strlen傳的參數(shù)不是一個(gè)字符串,則它會(huì)一直去找,直到 找到第一個(gè) ‘\0’,然后再計(jì)算其長度。
如 char a[]="chen"; char *p=a; 則strlen(p)=4;3.8、 當(dāng)數(shù)組作為一個(gè)形參時(shí),其實(shí)參是一個(gè)數(shù)組名(也可以是指針,其本質(zhì)就是指針),意義是首元素的首地址,則傳過去只影響形參的第一個(gè)元素。形參數(shù)組的地址被實(shí)參數(shù)組地址所綁定;
實(shí)參的大小會(huì)丟失,所以往往會(huì)傳一個(gè)int num 大小進(jìn)去。3.9、 結(jié)構(gòu)體做為形參時(shí),應(yīng)盡量用指針/地址方式來傳,因?yàn)榻Y(jié)構(gòu)體變量有時(shí)會(huì)占很大,效率很低。4.0、 int *p=&u; p存放的是變量u的地址,而&p的意思就是變量p本身的地址。4.1、當(dāng)要傳參的個(gè)數(shù)比較多時(shí),我們可以打包成一個(gè)結(jié)構(gòu)體,傳參的個(gè)數(shù)越多,其開銷就更大.4.2 一個(gè)函數(shù)作用其實(shí)就是輸入輸出,參數(shù)可以作為輸入,返回可以作為輸出,但是當(dāng)要返回多個(gè)輸出時(shí),這時(shí)候就不夠用了,所以常常返回值用來判斷程序又沒有出錯(cuò),而參數(shù)就是當(dāng)作輸入輸出的,輸入時(shí)可以加const表示它沒必要去修改,而輸出都是指針,因?yàn)橐淖兯闹担荒懿捎玫刂穫鬟f這種方式。比如:char *strcpy(char *dest,const char *src)
四、C語言復(fù)雜表達(dá)式
4.1、在表達(dá)式中,要看符號(hào)的優(yōu)先級(jí)和結(jié)合性。4.2、在理解內(nèi)存時(shí),內(nèi)存0地址在最底下,至上地址逐漸增加。4.3、int *p;是定義的一指針變量p,而int ( *p)[4];也是一個(gè)指針變量p;也可以這樣想:凡是遇到(*p)什么的判斷他是指針后,就可以說他是指針變量,包括函數(shù)指針。4.4、一個(gè)函數(shù) int max(int a ,int b); 則他的函數(shù)指針是 int ( *p ) (int ,int );其意思就是定義這個(gè)類型的函數(shù)指針變量p; p=max是賦值,引用是p();則相當(dāng)于max()調(diào)用這個(gè)函數(shù)。
函數(shù)指針必須和原函數(shù)的類型一樣。
4.5 函數(shù)指針其實(shí)就是為了做結(jié)構(gòu)體內(nèi)嵌指針的,這樣就構(gòu)成了高級(jí)語言中的類。再一個(gè)就是上述4.4中p=&max;也是可以的,它和p=max,值和意義都是一樣的,這個(gè)和數(shù)組有所區(qū)別,數(shù)組的a和&a的值雖然一樣,但是意義完全不一樣。int a[4];a有兩層意思,第一層是數(shù)組名,&a表示整個(gè)數(shù)組的地址,第二層表示首元素的首地址。4.6 int (*p[4])(int ,int)其意思是函數(shù)指針數(shù)組,一個(gè)4長度的數(shù)組,里面存了4個(gè)函數(shù)指針。* 4.7 printf在做輸出時(shí),其機(jī)制是緩沖行來輸出,即當(dāng)遇到一個(gè)\n后再打印出來,即使再多printf,沒有遇到\n,都不是一個(gè)一個(gè)打印。
'\r'是回車,'\n'是換行,前者使光標(biāo)到行首,后者使光標(biāo)下移一格,通常敲一個(gè)回車鍵,即是回車,又是換行(\r\n)。Unix中每行結(jié)尾只有“<換行>,即“\n”;Windows中每行結(jié)尾是“<換行><回車>”,即“\r\n”;Mac中每行結(jié)尾是“<回車>”。scanf("");里面不要加\n符。4.8 在一個(gè)c文件中,有時(shí)候會(huì)多次引入一個(gè).h文件,所以在寫.h文件時(shí),要寫
{#ifndef _FINE_#define _FINE_XXXXXXXXXXXXXXXXXXX#endif }
4.9、typedef int *intType; const intType p,其意思是指針p為const;4.9.1 對(duì)于typedef的定義:如typedef const int cdw; 可以這樣理解,typedef就是給一個(gè)類型區(qū)別名的,那么系統(tǒng)會(huì)自動(dòng)識(shí)別該類型,如果typedef const int char 則就報(bào)錯(cuò)。4.9.2 在開發(fā)中經(jīng)常會(huì)typedef int int32_t ; typedef short int16_t; 這樣做的目的是便于在不同平臺(tái)下的移植,如果當(dāng)在另一個(gè)平臺(tái)下,int 是64位的,但是我的項(xiàng)目中都是用的int32_t;
所以只需要修改int32_t就可以了,我可以讓他typedef short int32_t;這樣我只更改一次,其余的都改了,做到一改全改。** 4.9.3 int **p; int *a[4]; p=a;可以這樣理解:首先它是指針數(shù)組,既然是數(shù)組,則a即表示數(shù)組名又表示首元素的首地址,a[0]是一個(gè)一重指針,而a是a[0]的地址,那么a就是一個(gè)二重指針;{ 一重指針的地址就是二重指針變量,所以有p=a; 而 int a[4][3] ,a和一維數(shù)組的意思是一樣的,如 int a[3][6],int *p ;p=a[0];所以不能p=a,int *a[3][3],int **p;p=a[0];}** 4.9.4、二維數(shù)組是為了簡(jiǎn)化編程,平面型。數(shù)組以下標(biāo)示方式訪問其實(shí)是編譯器封裝起來的,實(shí)質(zhì)是指針訪問。int (*p)[5]; int a[2][5];則有 p=a; 關(guān)鍵是要把二維數(shù)組抽象成n行n列用指針訪問方式理解:二維數(shù)組可以看作是一個(gè)方格子的矩陣,比如a[2][5],那么就是2行5列的10個(gè)小格子,第一行可以收納起來變成一個(gè)指向一維數(shù)組的指針,第二行也是如此;
這樣收納后就變成了一個(gè)新的數(shù)組a[2],每一個(gè)格子存放的是它收納的第一個(gè)元素的地址,如a[0]存放的是第一行第一列元素的地址,“a”[1]存放的是第二行第一列的地址;
再與一維數(shù)組相聯(lián)系,一維數(shù)組名即表示數(shù)組名又表示數(shù)組第一個(gè)元素的地址,所以a[2][5]中的a表示“a"[2]數(shù)組第一個(gè)元素的地址;那么再把p=a;層層推遞,(p+i)表示指向第幾行的地址,*(p+i)表示取第幾行的值(而這個(gè)值存放的是第幾行一列元素的首地址),*(p+i)+j 表示指向第幾行第幾列的地址,最后在引用這個(gè)地址,*(*(p+i)+j)就表示第幾行第幾列的值了。
一重指針----------->一維數(shù)組二重指針----------->指針數(shù)組數(shù)組指針----------->二維數(shù)組函數(shù)指針----------->普通函數(shù)
五、數(shù)組&字符串&結(jié)構(gòu)體&共用體&枚舉(5.6?)
5.1、c語言中定義一個(gè)字符串: char a[6]={'l','i','n','u','x','\0'}; '\0'的字符編碼為0就是NULL;也就是說內(nèi)存中遇到0,翻譯成字符是就是'\0',或這是NULL;
char a[6]="linux";char*p="linux";
5.2、 sizeof(a)=6是運(yùn)算符,其意思是所占空間大小,包括字符串后面的‘\0',strlen(a)=5是一個(gè)函數(shù),其意思是字符串的長度。strlen( p);其中p只有是字符指針變量才有意義,它的形參是數(shù)組變量是沒有意義的,因?yàn)閟trlen是根據(jù)什么時(shí)候遇到 '\0',才結(jié)束測(cè)試字符串的長度,就算數(shù)組不初始化也是有長度的。
char *p="linux"; sizeof(p)永遠(yuǎn)等于4,因?yàn)閜是指針變量,存的是地址。所以總結(jié):sizeof()是拿來測(cè)數(shù)組的大小,strlen()是拿來測(cè)試字符串的長度。5.3、結(jié)構(gòu)體用 . 或者是 ->訪問內(nèi)部變量,其實(shí)質(zhì)是用的指針訪問。如
struct student{int a;double b;char c;}s1;
則s1.a =12;實(shí)質(zhì)就是int *p=(int *) &s1;*p=12 首先a是int 型,所以是強(qiáng)制類型 int * ,其次是就是算地址,然后強(qiáng)制類型,地址應(yīng)該是int 型然后加減,不然的話,系統(tǒng)s1.b=12.2;實(shí)質(zhì)就是double *p= (double *) ((int)&s1+4),*p=12.2; 不知道是以int 型加減還是以float型加減,還是以char型加減,所以 應(yīng)當(dāng) (int)&s1; 而且因?yàn)榈刂肥莝1.c=c;實(shí)質(zhì)就是 char *p=(char *) ((int)&s1+12); *p=c; 4字節(jié)的,所以必須是int型。&* 5.4、對(duì)齊方式:
(1)猜測(cè)如果是32位系統(tǒng),那么編譯器默認(rèn)是4字節(jié)對(duì)齊,64位系統(tǒng),那么編譯器默認(rèn)是8字節(jié)對(duì)齊,因?yàn)?2位或64位一次性訪問效率是最高的。
(2)
<1>結(jié)構(gòu)體首地址對(duì)齊(編譯器自身幫我們保證,會(huì)給它分配一個(gè)對(duì)齊的地址,因?yàn)榻Y(jié)構(gòu)體自身已經(jīng)對(duì)齊了,那么第一個(gè)變量也就自然對(duì)齊,所以我們才可以想象成第一個(gè)變量從0地址存放);
<2>結(jié)構(gòu)體內(nèi)部的各個(gè)變量要對(duì)齊。
<3>整個(gè)結(jié)構(gòu)體要對(duì)齊,因?yàn)槎x結(jié)構(gòu)體變量s1時(shí),再定義變量s2時(shí),如果s1沒有對(duì)齊,就坑了s2,所以也要保證整個(gè)結(jié)構(gòu)體對(duì)齊。
無論是按照幾字節(jié)對(duì)齊,我們都可以聯(lián)想到內(nèi)存實(shí)際的安排。1字節(jié)對(duì)齊那么不管int float double 類型,在每4個(gè)格子的內(nèi)存挨著存放。2字節(jié)對(duì)齊,也是一樣的想法,舉一個(gè)列子,如果第一個(gè)變量是char 型,第二個(gè)變量是int型,那么0地址存放char型,1地址空著,2地址存放int型地址部分,3地址存放int型地址部分,然后上排最右4、5地址存放int型高址部分。4字節(jié)對(duì)齊,如果第一個(gè)變量是char型,第二個(gè)變量是int型,那么0地址存放char型,1,2,3地址空著,從4地址開始存放int,最后給變量分配完內(nèi)存空間后,必須要保證整個(gè)結(jié)構(gòu)體對(duì)齊,下一個(gè)結(jié)構(gòu)體的存放起始地址是n字節(jié)對(duì)齊整數(shù)倍,如是4字節(jié)對(duì)齊,那么最后short算成4字節(jié) 以保證整個(gè)結(jié)構(gòu)體對(duì)齊。
整個(gè)結(jié)構(gòu)體對(duì)齊,如2字節(jié)對(duì)齊(2的整數(shù)倍),只要是0、2、4地址就行了,如果是4字節(jié)對(duì)齊(4的整數(shù)倍),就必須是0、4地址。8字節(jié)對(duì)齊(8的整數(shù)倍)
(3)猜測(cè)4字節(jié)/8字節(jié)其實(shí)是針對(duì)int型/double型的,比如0地址是char型,那么4字節(jié)對(duì)齊,int型、float型就必須從4地址開始存放,那么8字節(jié)對(duì)齊,int型就必須從4地址存放,double型就必須從8地址開始存放.小于幾字節(jié)對(duì)齊的那些,如char型和short型只要能按照規(guī)則存放就行了。
(4)對(duì)齊命令:<1>需要#prgama pack(n)開頭,以#pragma pack()結(jié)尾,定義一個(gè)區(qū)間,這個(gè)區(qū)間內(nèi)的對(duì)齊參數(shù)就是n。(不建議使用)
如:s1占5個(gè)字節(jié),s2占8字節(jié)(默認(rèn))
#pragma pack(1)struct stu1{
(結(jié)構(gòu)體本身以及變量) 對(duì)齊規(guī)則:2字節(jié)對(duì)齊(2的整數(shù)倍),只要是0、2、4地址就行了,4字節(jié)對(duì)齊(4的整數(shù)倍),就必須是0、4地址,8字節(jié)對(duì)齊(8的整數(shù)倍),就必須是0、8、16
char c;int a;//short d;}s1;struct stu2{char c;int a;//short d;}s2;
<2>gcc推薦的對(duì)齊指令__attribute__((packed)) __attribute__((aligned(n))),在VC中就不行,沒有定義這個(gè)命令
(1)__attribute__((packed))使用時(shí)直接放在要進(jìn)行內(nèi)存對(duì)齊的類型定義的后面,然后它起作用的范圍只有加了這個(gè)東西的這一個(gè)類型。packed的作用就是取消對(duì)齊訪問。
(2)__attribute__((aligned(n)))使用時(shí)直接放在要進(jìn)行內(nèi)存對(duì)齊的類型定義的后面,然后它起作用的范圍只有加了這個(gè)東西的這一個(gè)類型。它的作用是讓整個(gè)結(jié)構(gòu)體變量整體進(jìn)行n字節(jié)對(duì)齊(注意是結(jié)構(gòu)體變量整體n字節(jié)對(duì)齊,而不是結(jié)構(gòu)體內(nèi)各元素也要n字節(jié)對(duì)齊,內(nèi)部元素按照默認(rèn)對(duì)齊方式)
例子:
struct mystruct11{// 1字節(jié)對(duì)齊4字節(jié)對(duì)齊int a;// 44char b;// 12(1+1)short c;// 22}__attribute__((packed));typedef struct mystruct111{// 1字節(jié)對(duì)齊4字節(jié)對(duì)齊2字節(jié)對(duì)齊int a;// 44 4char b;// 12(1+1)2short c;// 22 2short d;// 2 4(2+2)2}__attribute__((aligned(1024))) My111;
5.5、offsetof宏:#define offsetof( TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)
(1)offsetof宏的作用是:用宏來計(jì)算結(jié)構(gòu)體中某個(gè)元素和結(jié)構(gòu)體首地址的偏移量(其實(shí)質(zhì)是通過編譯器來幫我們計(jì)算)。
(2)offsetof宏的原理:我們虛擬一個(gè)type類型結(jié)構(gòu)體變量,然后用type.member的方式來訪問那個(gè)member元素,繼而得到member相對(duì)于整個(gè)變量首地址的偏移量。
(3)學(xué)習(xí)思路:第一步先學(xué)會(huì)用offsetof宏,第二步再去理解這個(gè)宏的實(shí)現(xiàn)原理。
(TYPE *)0 這是一個(gè)強(qiáng)制類型轉(zhuǎn)換,把0地址強(qiáng)制類型轉(zhuǎn)換成一個(gè)指針,這個(gè)指針指向一個(gè)TYPE類型的結(jié)構(gòu)體變量。(實(shí)際上這個(gè)結(jié)構(gòu)體變量可能不存在,但是只要我不去解引用這個(gè)指針就不會(huì)出錯(cuò))。
((TYPE *)0)->MEMBER(TYPE *)0是一個(gè)TYPE類型結(jié)構(gòu)體變量的指針,通過指針指針來訪問這個(gè)結(jié)構(gòu)體變量的member元素,然后對(duì)這個(gè)元素取地址,又因?yàn)楦牡刂肥菑?地址開始算的,所以這個(gè)地址就是相對(duì)起始地址的偏移量。5.6 container_of宏:#define container_of(ptr, type, member) ({\const typeof(((type *)0)->member) * __mptr = (ptr);\(type *)((char *)__mptr - offsetof(type, member)); }) 兩條語句;,然后用{ } ,\表示提示編譯器本行因?yàn)槠聊徊粔颍溄酉乱恍小S?(也就是宏定義)時(shí),如果本行不夠要用 \ 提示編譯器接著是下一行的。必須要用 \ ,猜測(cè)因?yàn)楹甓x一行就算結(jié)束了。(1)作用:知道一個(gè)結(jié)構(gòu)體中某個(gè)元素的指針,反推這個(gè)結(jié)構(gòu)體變量的指針。有了container_of宏,我們可以從一個(gè)元素的指針得到整個(gè)結(jié)構(gòu)體變量的指針,繼而得到結(jié)構(gòu)體中其他元素的指針。
(2)typeof關(guān)鍵字的作用是:typepef(a)時(shí)由變量a得到a的類型,typeof就是由變量名得到變量數(shù)據(jù)類型的。
(3)這個(gè)宏的工作原理:先用typeof得到member元素的類型定義成一個(gè)指針,然后用這個(gè)指針減去該元素相對(duì)于整個(gè)結(jié)構(gòu)體變量的偏移量(偏移量用offsetof宏得到的),減去之后得到的就是整個(gè)結(jié)構(gòu)體變量的首地址了,
再把這個(gè)地址強(qiáng)制類型轉(zhuǎn)換為type *即可。5.7 p是一個(gè)地址,(int)p+6 和(char *)+6;效果是一樣的,第一種是將地址p當(dāng)作int型加減,第二種是將地址p做為char *指針,他每次加減都是一字節(jié)一字節(jié)相加減的,如果是 (int *)P+6,那么他每次加減都是按照4字節(jié)一跳。就相當(dāng)于加了+4*6;5.8 小端模式:變量的高地址存放在高地址,低地址存放在低地址;通信模式也要分大小端,先發(fā)送/接受的是高地址還是低地址,大端模式:變量的高地址存放在低地址,低地址存放在高地址;
測(cè)試:有用共用體 union 和指針方式來測(cè)試,基本思路是讓 int a=1; 看低地址 char 是0還是1 ;變量都是從地址開始存放,只是變量的高地址和低地址先存放誰不確定。
不能用位與來測(cè),因?yàn)榇娣藕妥x取都是按照某一個(gè)方式來的,結(jié)果永遠(yuǎn)都是一樣的。int a=1; char b=(char)a;這種方式不可以測(cè)試,因?yàn)椴还艽笮《耍家宰兞縜的低地址部分賦給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型):這樣寫默認(rèn)從第一個(gè)常量開始是0,1,2,3,4.........也可以自己賦值,但是每一個(gè)值是不一樣的,否則邏輯上出錯(cuò)。
enum week{sunday, sunday=1,moday, moday=5,tuseday, 然后其他常量以此遞增。wenzeday,friday,saterday,}today; today=sunday;* // 錯(cuò)誤1,枚舉類型重名,編譯時(shí)報(bào)錯(cuò):error: conflicting types for ‘DAY’typedef enum workday{MON, // MON = 1;TUE,WEN,THU,FRI,}DAY;typedef enum weekend{SAT,SUN,}DAY;*// /錯(cuò)誤2,枚舉成員重名,編譯時(shí)報(bào)錯(cuò):redeclaration of enumerator ‘MON’typedef enum workday{MON, // MON = 1;TUE,WEN,THU,FRI,}workday;typedef enum weekend{MON,SAT,SUN,}weekend;}
六、C語言宏定義與預(yù)處理、函數(shù)和函數(shù)庫(看博客strcyp原函數(shù))
6.1、源碼.c->(預(yù)處理)->預(yù)處理過的 .i 文件->(編譯)->匯編文件.S->(匯編)->目標(biāo)文件.o->(鏈接)->elf可執(zhí)行程序預(yù)處理用預(yù)處理器,編譯用編譯器,匯編用匯編器,鏈接用鏈接器,這幾個(gè)工具再加上其他一些額外的會(huì)用到的可用工具,合起來叫編譯工具鏈。gcc就是一個(gè)編譯工具鏈。<1>預(yù)處理的意義(1)編譯器本身的主要目的是編譯源代碼,將C的源代碼轉(zhuǎn)化成.S的匯編代碼。編譯器聚焦核心功能后,就剝離出了一些非核心的功能到預(yù)處理器去了。
(1)預(yù)處理器幫編譯器做一些編譯前的雜事。如:(1)#include(#include <>和#include ""的區(qū)別)
(2)注釋
(3)#if #elif #endif#ifdef
(4)宏定義
備注:gcc中只預(yù)處理不編譯的方法 -o生成可執(zhí)行文件名 -c只編譯不鏈接 -E 只預(yù)處理不編譯 -I ( 是大i,不是L )編譯時(shí)從某個(gè)路徑下尋找頭文件 . /當(dāng)前
(1)gcc編譯時(shí)可以給一些參數(shù)來做一些設(shè)置,譬如gcc xx.c -o xx可以指定可執(zhí)行程序的名稱;譬如gcc xx.c -c -o xx.o可以指定只編譯不連接,也可以生成.o的目標(biāo)文件。
(2)gcc -E xx.c -o xx.i可以實(shí)現(xiàn)只預(yù)處理不編譯。一般情況下沒必要只預(yù)處理不編譯,但有時(shí)候這種技巧可以用來幫助我們研究預(yù)處理過程,幫助debug程序。
(3)鏈接器:鏈接的時(shí)候是把目標(biāo)文件(二進(jìn)制)通過有序的排列組合起來,如 star.s main.c led.c 這三個(gè)源文件,分別被編譯成三個(gè)目標(biāo)文件 ,每個(gè)目標(biāo)文件有很多函數(shù)集合。鏈接的時(shí)候會(huì)根據(jù)運(yùn)行思路把這些雜亂的函數(shù)給排列組合起來,不是把目標(biāo)文件簡(jiǎn)單的排列組合。
(4)當(dāng)生成可執(zhí)行程序之后,這個(gè)可執(zhí)行程序里面有很多符號(hào)信息,有個(gè)符號(hào)表,里面的符號(hào)與一個(gè)地址相對(duì)應(yīng),如 函數(shù)名max對(duì)應(yīng)一個(gè)地址,雖然這個(gè)程序有符號(hào)信息,但是為什么還是可以執(zhí)行呢?因?yàn)槿鐆indows的exe程序,有專門的一套程序來執(zhí)行這個(gè).exe 文件,就好比壓縮文件,就有一套 “好壓”的軟件,然后去壓縮(執(zhí)行).rar .zip的文件,而這套程序就把這些符號(hào)信息給過濾掉,然后得到純凈的二進(jìn)制代碼,最后把他們加載到內(nèi)存中去。
(5) debug版本就是有符號(hào)信息,而Release版本就是純凈版本的。可用strip工具:strip是把可執(zhí)行程序中的符號(hào)信息給拿掉,以節(jié)省空間。(Debug版本和Release版本)objcopy:由可執(zhí)行程序生成可燒錄的鏡像bin文件。
6.2、預(yù)處理:
<1>頭文件有”“是本目錄去找,找不到就去庫頭文件找,和< > 只到庫頭文件去找,庫頭文件可以自己制作,用 -I ( 是大i,不是L )參數(shù)去尋找路徑。
頭文件在預(yù)處理時(shí),會(huì)把文件的內(nèi)容原封不動(dòng)的賦值到 c 文件里面。
<2>注釋:在預(yù)處理時(shí),把注釋全部拿掉。注意:#define *** 1 再判斷 #undef *** 2 時(shí),也是通過的。其意思是有沒有定義過***.
<3>條件編譯:當(dāng)作一個(gè)配置開關(guān) #define NUM 表示定義了NUM,則執(zhí)行下一條語句,且NUM用空格替代,而且預(yù)處理會(huì)刪掉條件編譯,留下正確的執(zhí)行語句。
<4>宏定義:#define cdw 1 在預(yù)處理階段,會(huì)替代那些宏,可以多重替代宏;也可以表示多個(gè)語句,如 #define cdw printf("cdw\n") ; printf("***\n"); cdw;這條語句會(huì)直接展開還有帶參宏,#define max(a,b) ((a)+(b)) 注意的是帶參宏一定要( ) 不然有時(shí)候會(huì)引起錯(cuò)誤,每一個(gè)”形參“都應(yīng)該要();#define year (365*24*60*60*60*60 ) 安理說是可以的,但是year是int型的已經(jīng)超過了范圍,所以要把它搞成無符號(hào)長整形。#define year (365*24*60*60*60*60ul ) 這樣才是正確的宏定義的變量是不占內(nèi)存空間的,直接替換減少開銷,但是變量替換是不進(jìn)行類型檢查;函數(shù)的變量要占用空間、要壓棧等操作,就需要很大的開銷,但是調(diào)用函數(shù)時(shí),編譯器會(huì)檢查函數(shù)變量的類型是否相同。內(nèi)聯(lián)函數(shù)集合普通函數(shù)、宏定義的兩個(gè)優(yōu)勢(shì),它直接就地展開,直接替換,減少開銷,同時(shí)編譯器也會(huì)檢查變量的類型。但是函數(shù)體積要小,不然效率反而很低,至于原因暫時(shí)不詳。6.3、內(nèi)聯(lián)函數(shù):對(duì)函數(shù)就地展開,像宏定義一樣,這樣減少開銷,同時(shí)也檢查變量的類型。但是必須函數(shù)的內(nèi)部體積小才用這種方式,以達(dá)到更好的效率。體積大的函數(shù)就作為普通函數(shù)。內(nèi)聯(lián)函數(shù)通過在函數(shù)定義前加inline關(guān)鍵字實(shí)現(xiàn)。* 6.4、條件編譯的應(yīng)用:做一個(gè)調(diào)試開關(guān)。#define DEBUG #undef DEBUG 是注銷 DEBUG 宏#ifdef DEBUG#define debug(x) printf(x)#else#define debug(x)#endif6.5、函數(shù):
(1)整個(gè)程序分成多個(gè)源文件,一個(gè)文件分成多個(gè)函數(shù),一個(gè)函數(shù)分成多個(gè)語句,這就是整個(gè)程序的組織形式。這樣組織的好處在于:分化問題、便于編寫程序、便于分工。
(2)函數(shù)的出現(xiàn)是人(程序員和架構(gòu)師)的需要,而不是機(jī)器(編譯器、CPU)的需要。
(3)函數(shù)的目的就是實(shí)現(xiàn)模塊化編程。說白了就是為了提供程序的可移植性。
<1>函數(shù)書寫的一般原則:
第一:遵循一定格式。函數(shù)的返回類型、函數(shù)名(男女廁所)、參數(shù)列表(太多用結(jié)構(gòu)體)等。
第二:一個(gè)函數(shù)只做一件事:函數(shù)不能太長也不宜太短(一個(gè)屏幕的大小),原則是一個(gè)函數(shù)只做一件事情。
第三:傳參不宜過多:在ARM體系下,傳參不宜超過4個(gè)。如果傳參確實(shí)需要多則考構(gòu)體打包考慮。
第四:盡量少碰全局變量:函數(shù)最好用傳參返回值來和外部交換數(shù)據(jù),不要用全局變量。
<2> 之所以函數(shù)能被調(diào)用,根本實(shí)質(zhì)是在編譯時(shí),檢查到了該函數(shù)的聲明,不是因?yàn)楹瘮?shù)定義了(當(dāng)然也要定義才行,只是不是本質(zhì))。6.6、遞歸函數(shù):自己調(diào)用自己的函數(shù),常用舉例:階乘 int jiecheng( int n) 斐波那契數(shù)例:f(n)=f(n-1)+f(n-2) n>2的正整數(shù)
{ int he(int n)
注意:if(n<1) if(3==n||4==n)
棧溢出:遞歸函數(shù)會(huì)不停的耗費(fèi)棧空間 { {所以要注意遞歸不要太多 printf("error\n"); return 1;收斂性:必須 要有一個(gè)終止遞歸的條件 } }
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、函數(shù)庫:<1>靜態(tài)鏈接庫其實(shí)就是商業(yè)公司將自己的函數(shù)庫源代碼經(jīng)過只編譯不連接形成.o的目標(biāo)文件,然后用ar工具將.o文件歸檔成.a的歸檔文件(.a的歸檔文件又叫靜態(tài)鏈接庫文件)。
商業(yè)公司通過發(fā)布.a庫文件和.h頭文件來提供靜態(tài)庫給客戶使用;客戶拿到.a和.h文件后,通過.h頭文件得知庫中的庫函數(shù)的原型,然后在自己的.c文件中直接調(diào)用這些庫文件,在連接的時(shí)候鏈接器會(huì)去.a文件中拿出被調(diào)用的那個(gè)函數(shù)的編譯后的.o二進(jìn)制代碼段鏈接進(jìn)去形成最終的可執(zhí)行程序。
<2>動(dòng)態(tài)鏈接庫本身不將庫函數(shù)的代碼段鏈接入可執(zhí)行程序,只是做個(gè)標(biāo)記。然后當(dāng)應(yīng)用程序在內(nèi)存中執(zhí)行時(shí),運(yùn)行時(shí)環(huán)境發(fā)現(xiàn)它調(diào)用了一個(gè)動(dòng)態(tài)庫中的庫函數(shù)時(shí),會(huì)去加載這個(gè)動(dòng)態(tài)庫到內(nèi)存中,然后以后不管有多少個(gè)應(yīng)用程序去調(diào)用這個(gè)庫中的函數(shù)都會(huì)跳轉(zhuǎn)到第一次加載的地方去執(zhí)行(不會(huì)重復(fù)加載)。
也就是在運(yùn)行時(shí),會(huì)把庫函數(shù)代碼放入內(nèi)存中,然后多個(gè)程序要用到庫函數(shù)時(shí),就從這段內(nèi)存去找,而靜態(tài)鏈接對(duì)于多程序就是重復(fù)使用庫函數(shù),比較占內(nèi)存。
(1) gcc中編譯鏈接程序默認(rèn)是使用動(dòng)態(tài)庫的,要想靜態(tài)鏈接需要顯式用-static來強(qiáng)制靜態(tài)鏈接。
(2) 庫函數(shù)的使用需要注意3點(diǎn):第一,包含相應(yīng)的頭文件;第二,調(diào)用庫函數(shù)時(shí)注意函數(shù)原型;第三,有些庫函數(shù)鏈接時(shí)需要額外用-lxxx來指定鏈接;第四,如果是動(dòng)態(tài)庫,要注意-L指定動(dòng)態(tài)庫的地址。6.8、常見的兩個(gè)庫函數(shù):<1>C庫中字符串處理函數(shù)包含在string.h中,這個(gè)文件在ubuntu系統(tǒng)中在/usr/include中字符串函數(shù) 如:memcpy(內(nèi)存字符串復(fù)制,直接復(fù)制到目標(biāo)空間)確定src和dst不會(huì)overlap重復(fù),則使用memcpy效率高memmove(內(nèi)存字符串復(fù)制,先復(fù)制到一個(gè)內(nèi)存空間,然后再復(fù)制到目標(biāo)空間)確定會(huì)overlap或者不確定但是有可能overlap,則使用memove比較保險(xiǎn)memset strncmpmemcmp strdup???? memchr strndupstrcpy strchrstrncpy strstrstrcat strtokstrncat 。。。strcmp
<2> 數(shù)學(xué)函數(shù):math.h 需要加 -lm 就是告訴鏈接器到libm中去查找用到的函數(shù)。
C鏈接器的工作特點(diǎn):因?yàn)閹旌瘮?shù)有很多,鏈接器去庫函數(shù)目錄搜索的時(shí)間比較久。為了提升速度想了一個(gè)折中的方案:鏈接器只是默認(rèn)的尋找?guī)讉€(gè)最常用的庫,如果是一些不常用的庫中的函數(shù)被調(diào)用,需要程序員在鏈接時(shí)明確給出要擴(kuò)展查找的庫的名字。
鏈接時(shí)可以用-lxxx來指示鏈接器去到libxxx.so中去查找這個(gè)函數(shù)。6.9、自制靜態(tài)鏈接庫:
(1)第一步:自己制作靜態(tài)鏈接庫,首先使用gcc -c只編譯不連接,生成.o文件;然后使用ar工具進(jìn)行打包成.a歸檔文件庫名不能隨便亂起,一般是lib+庫名稱,后綴名是.a表示是一個(gè)歸檔文件注意:制作出來了靜態(tài)庫之后,發(fā)布時(shí)需要發(fā)布.a文件和.h文件。
(2)第二步:使用靜態(tài)鏈接庫,把.a和.h都放在我引用的文件夾下,然后在.c文件中包含庫的.h,然后直接使用庫函數(shù)。
備注:
<1>.a 文件,前綴一定要加lib ,如 lib***.a ; 鏈接屬性 -l(小L),表示庫名,屬性-L表示庫的路徑。所以:gcc cdw.c -o cdw -l*** -L ./include -I(大i) ./include
<2> 頭文件“ ”表示外部自定義,如果沒加路徑屬性,默認(rèn)當(dāng)前路徑找,如果在其他文件夾下,必須用 -I(大i) 路徑。用<>表示的頭文件一種是在編譯器下的庫文件找,第二種是自己定義的庫文件找,但是要定義其路徑。
<3> 在makefile文件中用到gcc/arm-gcc 那么在shell中就用相應(yīng)的編譯器 gcc/arm-gcc .
<4> nm ./include/libmax.a 查看max庫的信息,有哪些 .o 文件 .o文件有哪些函數(shù)。
舉例:makefile: arm-gcc aston.c -o aston.o -carm-ar -rc libaston.a aston.o6.9.1、自制動(dòng)態(tài)鏈接庫:
<1>動(dòng)態(tài)鏈接庫的后綴名是.so(對(duì)應(yīng)windows系統(tǒng)中的dll),靜態(tài)庫的擴(kuò)展名是.a .
<2>第一步:創(chuàng)建一個(gè)動(dòng)態(tài)鏈接庫。gcc aston.c -o aston.o -c -fPIC (-fPIC表示設(shè)置位置無關(guān)碼)gcc -o libaston.so aston.o -shared (-shared表示使用共享庫)注意:做庫的人給用庫的人發(fā)布庫時(shí),發(fā)布libxxx.so和xxx.h即可。
第二步:使用自己創(chuàng)建的共享庫。gcc cdw.c -o cdw -lmax.so -L ./動(dòng)態(tài)鏈接庫 -I ./動(dòng)態(tài)鏈接庫
第三步:上述編譯成功了,但是在 ./cdw 執(zhí)行時(shí)會(huì)報(bào)錯(cuò),原因是采用動(dòng)態(tài)鏈接,在可執(zhí)行文件只是做了一個(gè)標(biāo)記,標(biāo)記是使用了哪個(gè)函數(shù)庫的哪個(gè)函數(shù)。
并沒有將庫函數(shù)加載到源文件中,所以可執(zhí)行文件很小,在執(zhí)行時(shí),需要立即從系統(tǒng)里面找到使用到的函數(shù)庫,然后加載到內(nèi)存中,在linux系統(tǒng)中默認(rèn)是從 /usr/bin 中尋找,(不確定:如果使用shell中運(yùn)行)會(huì)先執(zhí)行環(huán)境變量的路徑然后再查找 /usr/bin;所以我們可以用兩種辦法解決運(yùn)行的問題
第四步:將動(dòng)態(tài)庫 libmax.so 復(fù)制到 /usr/lib 下面,但是如果以后所有的庫都這樣放的話,會(huì)越來越臃腫,導(dǎo)致運(yùn)行速度變慢(一個(gè)一個(gè)查找);或者是新添加一個(gè)環(huán)境變量
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/mnt/hgfs/share/include (將庫 libmax.so 復(fù)制到這個(gè)路徑下面)這樣就可以運(yùn)行了。
<3>使用 ldd 命令判斷一個(gè)可執(zhí)行文件是否能運(yùn)行成功;ldd cdwlinux-gate.so.1 => (0xb77a8000)libmax.so => not found //發(fā)現(xiàn) not found意思就是沒有找到對(duì)應(yīng)的函數(shù)庫libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75e2000)/lib/ld-linux.so.2 (0xb77a9000)
七、存儲(chǔ)類&作用域&生命周期&鏈接屬性
7.1、概念詞:存儲(chǔ)類(棧、堆、數(shù)據(jù)區(qū)、.bss段、.text段)作用域(代碼塊作用范圍,也就是變量作用的范圍)生命周期(變量的誕生和死亡)鏈接屬性(外鏈接屬性、內(nèi)鏈接屬性、無連接屬性)7.2、Linux下的內(nèi)存映射(分配情況、組織情況):見圖內(nèi)存映射。其中有關(guān)進(jìn)程的空間,如進(jìn)程控制塊、頁表等都是在內(nèi)核里面的。文件區(qū)是映射外部文件的,如打開記事本,那么這個(gè)文件臨時(shí)存放在文件區(qū)域。(見引用資料)
問題:虛擬地址技術(shù)?解決:后期在Linux應(yīng)用/網(wǎng)絡(luò)編程會(huì)講。
OS下和裸機(jī)下C程序加載執(zhí)行的差異?解決:在arm裸機(jī)第十六部分有介紹。7.3、存儲(chǔ)類關(guān)鍵字:
<1> auto 自動(dòng)的(一個(gè)用法:修飾局部變量,在定義變量時(shí)可以省略) 【外鏈接:與第二個(gè)c文件鏈接】【內(nèi)鏈接:只與本c文件鏈接】【無連接:就是無鏈接】
<2> static 靜態(tài)的(有兩個(gè)用法,第一個(gè)是修飾局部變量,意思是當(dāng)作全局變量,存放在數(shù)據(jù)區(qū),作用域只是定義的那個(gè)函數(shù)范圍,生命周期和整個(gè)程序一樣,屬于無連接
第二個(gè)是修改全局變量/函數(shù),意思是這個(gè)全局變量/函數(shù)只在當(dāng)前c文件有效,其他c文件是不能使用它的,屬于內(nèi)鏈接,普通全局變量屬于外連接)<3>register 寄存器(一個(gè)用法,修飾變量,作用是讓編譯器把這個(gè)變量放在寄存器中,當(dāng)這個(gè)變量頻繁的被使用時(shí),用這個(gè)方法可以提高效率,但有時(shí)候不一定就放在寄存器,因?yàn)榧拇嫫魇怯邢薜模瑳]有剩余的寄存器了)
<4>extern (一個(gè)用法,修飾全局變量,表示該文件要使用的這個(gè)變量,在另外一個(gè)c文件中已經(jīng)定義了,其一個(gè)聲明的作用,不能初始化)
<5>volatile (一個(gè)用法,修飾變量,表示對(duì)這個(gè)變量的語句不要去優(yōu)化)
(1) volatile的字面意思:可變的、易變的。C語言中volatile用來修飾一個(gè)變量,表示這個(gè)變量可以被編譯器之外的東西改變。編譯器之內(nèi)的意思是變量的值的改變是代碼的作用,編譯器之外的改變就是這個(gè)改變不是代碼造成的,或者不是當(dāng)前代碼造成的,編譯器在編譯當(dāng)前代碼時(shí)無法預(yù)知。譬如在中斷處理程序isr中更改了這個(gè)變量的值,譬如多線程中在別的線程更改了這個(gè)變量的值,譬如硬件自動(dòng)更改了這個(gè)變量的值(一般這個(gè)變量是存在寄存器,或許當(dāng)其他進(jìn)程要用到這個(gè)寄存器時(shí),就把這個(gè)寄存器的變量給改變了,同時(shí)也就改變了這個(gè)變量)
(2) 以上說的三種情況(中斷isr中引用的變量,多線程中共用的變量,硬件會(huì)更改的變量)都是編譯器在編譯時(shí)無法預(yù)知的更改,此時(shí)應(yīng)用使用volatile告訴編譯器這個(gè)變量屬于這種(可變的、易變的)情況。編譯器在遇到volatile修飾的變量時(shí)就不會(huì)對(duì)改變量的訪問進(jìn)行優(yōu)化,就不會(huì)出現(xiàn)錯(cuò)誤。
(3) 編譯器的優(yōu)化在一般情況下非常好,可以幫助提升程序效率。但是在特殊情況(volatile)下,變量會(huì)被編譯器想象之外的力量所改變,此時(shí)如果編譯器沒有意識(shí)到而去優(yōu)化則就會(huì)造成優(yōu)化錯(cuò)誤,優(yōu)化錯(cuò)誤就會(huì)帶來執(zhí)行時(shí)錯(cuò)誤。
而且這種錯(cuò)誤很難被發(fā)現(xiàn)。
(4) volatile是程序員意識(shí)到需要volatile然后在定義變量時(shí)加上volatile,如果你遇到了應(yīng)該加volatile的情況而沒有加程序可能會(huì)被錯(cuò)誤的優(yōu)化。如果在不應(yīng)該加volatile而加了的情況程序不會(huì)出錯(cuò)只是會(huì)降低效率。
所以我們對(duì)于volatile的態(tài)度應(yīng)該是:正確區(qū)分,該加的時(shí)候加不該加的時(shí)候不加,如果不能確定該不該加為了保險(xiǎn)起見就加上。
舉例子1:int a=3 ,b,c;b=a;c=b;
那么編譯器會(huì)優(yōu)化成 c=b=a=3; 如果此時(shí)遇到上述三種情況,突然改變了a的值,那么,對(duì)于沒有優(yōu)化前是對(duì)的,但是對(duì)于優(yōu)化后,那么c仍然是3,就會(huì)出錯(cuò)。
所以當(dāng)我們程序員知道這個(gè)變量會(huì)發(fā)生改變時(shí),就應(yīng)該加 volatile,提示編譯器不要幫我們做優(yōu)化。舉列子2:
int square(volatile int *ptr) { return *ptr * *ptr; }
這段代碼的有個(gè)惡作劇。這段代碼的目的是用來返指針*ptr指向值的平方,但是,由于*ptr指向一個(gè)volatile型參數(shù),編譯器將產(chǎn)生類似下面的代碼:
int square(volatile int *ptr){ int a,b; a = *ptr; b = *ptr; return a * b; }
由于*ptr的值可能被意想不到地該變,因此a和b可能是不同的。結(jié)果,這段代碼可能返不是你所期望的平方值!正確的代碼如下:
long square(volatile int *ptr) { int a; a = *ptr; return a * a; }
<6> restrict (1)c99中才支持的,所以很多延續(xù)c89的編譯器是不支持restrict關(guān)鍵字,gcc支持的。
(2)restrict 作用是讓編譯器對(duì)這個(gè)變量做一些優(yōu)化,讓它提高效率。下面的網(wǎng)站有列子。
(3)restrict只用來修飾指針,不能修飾普通變量,它表示只能該指針才能修改它的內(nèi)容。如用memcpy時(shí),兩個(gè)內(nèi)存存在重疊的現(xiàn)象。
(4)https://blog.chinaunix.net/uid-22197900-id-359209.html (這個(gè)網(wǎng)站里面有詳細(xì)的例子)
(5)memcpy和memmove的區(qū)別 void *memcpy( void * restrict dest ,const void * restrict src,sizi_t n);這樣它可以優(yōu)化成memmove原理的方式(當(dāng)存在重疊時(shí),先復(fù)制到一段內(nèi)存空間,然后再把它復(fù)制到目的空間)7.4、作用域:
(1)全局變量起名字一般是 g_a;
(2)名字加前綴表示7.5、總結(jié):<1>局部變量地址由運(yùn)行時(shí)在棧上分配得到,多次執(zhí)行時(shí)地址不一定相同,函數(shù)不能返回該類變量的地址(指針)作為返回值。
<2>為什么要分為數(shù)據(jù)段和.bbs段?因?yàn)楫?dāng)加載到內(nèi)存重定位時(shí),如果這些數(shù)據(jù)(包括0)一個(gè)一個(gè)的復(fù)制,會(huì)降低效率,為0的變量,直接清內(nèi)存就可以了,這樣提高效率。
<3>在頭文件聲明全局變量時(shí), extern int a; 聲明函數(shù)就是 void int max(int a, int b);
<4>寫程序盡量避免使用全局變量,尤其是非static類型的全局變量。能確定不會(huì)被其他文件引用的全局變量一定要static修飾。(因?yàn)槿肿兞空純?nèi)存的時(shí)間是最長的,要看你的變量是不是需要這么長的時(shí)間,這樣可以節(jié)約內(nèi)存空)
八、一些雜散但值得討論的問題
8.1、操作系統(tǒng)的理解:
<1>它是一個(gè)管理階級(jí)者,管理所有資源,負(fù)責(zé)調(diào)配優(yōu)化等操作。這樣想象,就像裸機(jī)一樣的話,要實(shí)現(xiàn)LED閃爍的進(jìn)程、串口傳輸?shù)倪M(jìn)程、蜂鳴器等這些,他們都要搶占一些資源,這個(gè)時(shí)候沒有操作系統(tǒng),就亂成一鍋粥,當(dāng)有了OS的時(shí)候,它就專門負(fù)責(zé)資源的調(diào)配,讓各個(gè)任務(wù)都能很好的實(shí)施,起一個(gè)決策者的作用。
<2>如果我們要做一個(gè)產(chǎn)品,軟件系統(tǒng)到底應(yīng)該是裸機(jī)還是基于操作系統(tǒng)呢?本質(zhì)上取決于產(chǎn)品本身的復(fù)雜度。只有極簡(jiǎn)單的功能、使用極簡(jiǎn)單的CPU(譬如單片機(jī))的產(chǎn)品才會(huì)選擇用裸機(jī)開發(fā);一般的復(fù)雜性產(chǎn)品都會(huì)選擇基于操作系統(tǒng)來開發(fā)。
<3>操作系統(tǒng)負(fù)責(zé)管理和資源調(diào)配,應(yīng)用程序負(fù)責(zé)具體的直接勞動(dòng),他們之間的接口就是API函數(shù)。當(dāng)應(yīng)用程序需要使用系統(tǒng)資源(譬如內(nèi)存、譬如CPU、譬如硬件操作)時(shí)就通過API向操作系統(tǒng)發(fā)出申請(qǐng),然后操作系統(tǒng)響應(yīng)申請(qǐng)幫助應(yīng)用程序執(zhí)行功能。8.2、C庫函數(shù)和API的關(guān)系:
<1>從內(nèi)核的角度看,需要考慮提供哪些有用的API函數(shù),并不需要關(guān)注它們?nèi)绾伪皇褂谩9示幊虝r(shí)用API函數(shù)會(huì)感覺到不好用,沒有做優(yōu)化。系統(tǒng)只負(fù)責(zé)做出一個(gè)可以用的API,沒有考慮到用戶使用的方便性。所以c庫函數(shù)對(duì)API做了一些優(yōu)化,讓用戶使用庫函數(shù)更容易達(dá)到我們想要的目的。
<2>庫函數(shù)實(shí)質(zhì)還是用的API,或者調(diào)用了一個(gè)API,也或者調(diào)用了更多的API,只不過是做了更多的優(yōu)化。比如 庫函數(shù)fopen ,而API是open.
<3>有的庫函數(shù)沒有用到API,比如strcpy函數(shù)(復(fù)制字符串)和atoi函數(shù)(轉(zhuǎn)換ASCII為整數(shù)),因?yàn)樗鼈儾⒉恍枰騼?nèi)核請(qǐng)求任何服務(wù)。8.3、不同平臺(tái)(windows、linux、裸機(jī))下庫函數(shù)的差異
(1)不同操作系統(tǒng)API是不同的,但是都能完成所有的任務(wù),只是完成一個(gè)任務(wù)所調(diào)用的API不同。
(2)庫函數(shù)在不同操作系統(tǒng)下也不同,但是相似性要更高一些。這是人為的,因?yàn)槿讼乱庾R(shí)想要屏蔽不同操作系統(tǒng)的差異,因此在封裝API成庫函數(shù)的時(shí)候,盡量使用了同一套接口,所以封裝出來的庫函數(shù)挺像的。
但是還是有差異,所以在一個(gè)操作系統(tǒng)上寫的應(yīng)用程序不可能直接在另一個(gè)操作系統(tǒng)上面編譯運(yùn)行。于是乎就有個(gè)可移植性出來了。
(3)跨操作系統(tǒng)可移植平臺(tái),譬如QT、譬如Java語言。8.4、
<1>main()函數(shù)的寫法:int main(int argc,char **argv) ; int main(int argc ,char *argv[ ] ); 這兩種寫法是一樣的。二重指針等同于指針數(shù)組。
<2>不管是主函數(shù)還是功能函數(shù),它都應(yīng)該有一個(gè)返回值,而主函數(shù)的返回值是給調(diào)用的那個(gè)人的/main函數(shù)從某種角度來講代表了我當(dāng)前這個(gè)程序,或者說代表了整個(gè)程序。main函數(shù)的開始意味著整個(gè)程序開始執(zhí)行,main函數(shù)的結(jié)束返回意味著整個(gè)程序的結(jié)束。誰執(zhí)行了這個(gè)程序,誰就調(diào)用了main。誰執(zhí)行了程序?或者說程序有哪幾種被調(diào)用執(zhí)行的方法?一個(gè)程序當(dāng)然會(huì)運(yùn)行,那么就是調(diào)用了main( ).
<3>inux下一個(gè)新程序執(zhí)行的本質(zhì)
(1)表面來看,linux中在命令行中去./xx執(zhí)行一個(gè)可執(zhí)行程序
(2)我們還可以通過shell腳本來調(diào)用執(zhí)行一個(gè)程序
(3)我們還可以在程序中去調(diào)用執(zhí)行一個(gè)程序(fork exec)
總結(jié):我們有多種方法都可以執(zhí)行一個(gè)程序,但是本質(zhì)上是相同的。linux中一個(gè)新程序的執(zhí)行本質(zhì)上是一個(gè)進(jìn)程的創(chuàng)建、加載、運(yùn)行、消亡。linux中執(zhí)行一個(gè)程序其實(shí)就是創(chuàng)建一個(gè)新進(jìn)程然后把這個(gè)程序丟進(jìn)這個(gè)進(jìn)程中去執(zhí)行直到結(jié)束。
新進(jìn)程是被誰開啟?在linux中進(jìn)程都是被它的父進(jìn)程fork出來的。
分析:命令行本身就是一個(gè)進(jìn)程,在命令行底下去./xx執(zhí)行一個(gè)程序,其實(shí)這個(gè)新程序是作為命令行進(jìn)程的一個(gè)字進(jìn)程去執(zhí)行的。
總之一句話:一個(gè)程序被它的父進(jìn)程所調(diào)用。
結(jié)論:main函數(shù)返回給調(diào)用這個(gè)函數(shù)的父進(jìn)程。父進(jìn)程要這個(gè)返回值干嘛?父進(jìn)程調(diào)用子進(jìn)程來執(zhí)行一個(gè)任務(wù),然后字進(jìn)程執(zhí)行完后通過main函數(shù)的返回值返回給父進(jìn)程一個(gè)答復(fù)。這個(gè)答復(fù)一般是表示子進(jìn)程的任務(wù)執(zhí)行結(jié)果完成了還是錯(cuò)誤了。
(0表示執(zhí)行成功,負(fù)數(shù)表示失敗,正規(guī)的要求返回失敗的原因,返回-1表示什么,返回-2又表示什么,然后父進(jìn)程好做相應(yīng)的處理)
(4) main函數(shù)的返回值應(yīng)當(dāng)是int 型。父進(jìn)程要求是int型的,如果寫成 float 型,則返回就為0,這樣是要出錯(cuò)的。8.5 用shell腳本來看main()的返回值。如:#!/bin/sh 其文本格式為 .sh./a.outecho $?8.6、argc、argv與main函數(shù)的傳參:當(dāng)我們的父進(jìn)程不需要傳參時(shí),就用 int main(void);當(dāng)我們需要傳參時(shí),就應(yīng)該是 int main(int argv ,char *argc[ ]);它默認(rèn)本身就是一個(gè)參數(shù),占了argv[0]這個(gè)位置,它里面存的是 ./a.out (這個(gè)相應(yīng)變化)
如:./a.out boy girl ;則 argv=3; argc[0]="./a.out"; argc[1]="boy"; argc[2]="girl" ; printf("%s\n",argc[0]);
解釋:argv表示傳了多少個(gè)參數(shù),argc實(shí)質(zhì)是存的一個(gè)指針,也就是一個(gè)地址,只是沒有一個(gè)被綁定的變量名而已。這個(gè)地址指向一個(gè)字符串,一般字符串都和指針相關(guān)。所以可以稱之為字符串?dāng)?shù)組,每一個(gè)都存了一個(gè)字符串。
在程序內(nèi)部如果要使用argc,那么一定要先檢驗(yàn)argv,先檢驗(yàn)個(gè)數(shù),然后使用。8.7、void類型的本質(zhì):即使空型又是未知類型,看具體情況。比如一個(gè)函數(shù)void表示不返回, void *malloc(20);就是未知類型。
(1)編程語言分2種:強(qiáng)類型語言和弱類型語言。強(qiáng)類型語言中所有的變量都有自己固定的類型,這個(gè)類型有固定的內(nèi)存占用,有固定的解析方法;弱類型語言中沒有類型的概念,所有變量全都是一個(gè)類型(一般都是字符串的),程序在用的時(shí)候再根據(jù)需要來處理變量。就如:makefile、html語言。
(2)C語言就是典型的強(qiáng)類型語言,C語言中所有的變量都有明確的類型。因?yàn)镃語言中的一個(gè)變量都要對(duì)應(yīng)內(nèi)存中的一段內(nèi)存,編譯器需要這個(gè)變量的類型來確定這個(gè)變量占用內(nèi)存的字節(jié)數(shù)和這一段內(nèi)存的解析方法。
(3)void類型的正確的含義是:不知道類型,不確定類型,還沒確定類型、未知類型,但是將來一定有類型。
(4)void *a;(編譯器可以通過)定義了一個(gè)void類型的變量,含義就是說a是一個(gè)指針,而且a肯定有確定的類型,只是目前我還不知道a的類型,還不確定,所以標(biāo)記為void。
void “修飾”的是指針,因?yàn)橹羔樉褪莾?nèi)存地址,它不知道指向的另一個(gè)變量是哪一種類型,而變量一定是確定的,void a;就是錯(cuò)誤的。8.9、C語言中的NULL
NULL在C/C++中的標(biāo)準(zhǔn)定義
(1)NULL不是C語言關(guān)鍵字,本質(zhì)上是一個(gè)宏定義,其保護(hù)指針的作用,不要讓他亂開槍。
(2)NULL的標(biāo)準(zhǔn)定義:
#ifdef _cplusplus // 條件編譯c++環(huán)境#define NULL 0#else#define NULL (void *)0 // 這里對(duì)應(yīng)C語言的情況#endif
解釋:C++的編譯環(huán)境中,編譯器預(yù)先定義了一個(gè)宏_cplusplus,程序中可以用條件編譯來判斷當(dāng)前的編譯環(huán)境是C++的還是C的。
NULL的本質(zhì)解析:NULL的本質(zhì)是0,但是這個(gè)0不是當(dāng)一個(gè)數(shù)字解析,而是當(dāng)一個(gè)內(nèi)存地址來解析的,這個(gè)0其實(shí)是0x00000000,代表內(nèi)存的0地址。(void *)0這個(gè)整體表達(dá)式表示一個(gè)指針,這個(gè)指針變量本身占4字節(jié),地址在哪里取決于指針變量本身,但是這個(gè)指針變量的值是0,也就是說這個(gè)指針變量指向0地址(實(shí)際是0地址開始的一段內(nèi)存)。如 char *p=NULL; 因?yàn)?地址本身就不是我們來訪問的,所以 *p時(shí)是不可訪問的。在程序運(yùn)行的邏輯上就不會(huì)出錯(cuò)。
正規(guī)寫:
int *p = NULL;// 定義p時(shí)立即初始化為NULLp = xx;if (NULL != p){*p // 在確認(rèn)p不等于NULL的情況下才去解引用p}
(1)'\0'是一個(gè)轉(zhuǎn)義字符,他對(duì)應(yīng)的ASCII編碼值是0,內(nèi)存值是0,一個(gè)char空間。
(2)'0'是一個(gè)字符,他對(duì)應(yīng)的ASCII編碼值是48,內(nèi)存值是int型48,一個(gè)char空間。
(3)0是一個(gè)數(shù)字,沒有ASCll編碼, 內(nèi)存值是int型0,一個(gè)int空間。
(4)NULL是一個(gè)表達(dá)式,是強(qiáng)制類型轉(zhuǎn)換為void *類型的0,內(nèi)存值是0(內(nèi)存地址),一個(gè)int空間。8.9.1、運(yùn)算中的臨時(shí)匿名變量
<1>“小動(dòng)作”:高級(jí)語言在運(yùn)算中允許我們大跨度的運(yùn)算。意思就是低級(jí)語言中需要好幾步才能完成的一個(gè)運(yùn)算,在高級(jí)語言中只要一步即可完成。譬如C語言中一個(gè)變量i要加1,在C中只需要i++即可,看起來只有一句代碼。但實(shí)際上翻譯到匯編階段需要3步才能完成:第1步從內(nèi)存中讀取i到寄存器,
第2步對(duì)寄存器中的i進(jìn)行加1,第3步將加1后的i寫回內(nèi)存中的i。
<2> float a=12.3; int b=(int)a; (int )a 就是匿名變量;先找一個(gè)內(nèi)存空間,里面存(int)a;然后把這個(gè)值賦值給b;最后匿名值銷毀。
float a; int b=10; a=b/3; 左邊是3.00000;右邊是3;其中有個(gè)匿名變量,先找一個(gè)內(nèi)存空間,里面存 b/3; 然后把它再轉(zhuǎn)換成float型,再賦值個(gè)a;最后匿名值銷毀。8.9.2 分析DEBUG宏學(xué)習(xí)級(jí):
#define DEBUG #undef DEBUG 是注銷 DEBUG 宏#ifdef DEBUG#define debug(x) printf(x)#else#define debug(x)#endif
應(yīng)用級(jí):
#ifdefDEBUG#define DBG(...) fprintf(stderr, " DBG(%s, %s( ), %d): ", __FILE__, __FUNCTION__, __LINE__); fprintf(stderr, __VA_ARGS__)#else#define DBG(...)#endif
解 釋:<1>...表示變參,提示編譯器不要對(duì)參數(shù)個(gè)數(shù)斤斤計(jì)較,不要報(bào)錯(cuò);其實(shí)完全可以把 ...換成 cdw 也是可以的,只是非要裝一下而已。
<2> _FILE_ 和 _FUNCTION_和 _LINE_ 都是c庫函數(shù)的宏定義,分別表示要輸出的這句話屬于哪個(gè)文件名、屬于哪個(gè)函數(shù)名、在第幾行。
<3> 在 fprintf(stderr,"cdw");其中stderr是c庫函數(shù)中宏定義了的,這是VC6.0找到的 #define stderr (&_iob[2]) ;也就是說stderr是一個(gè)被宏定義了的指針,它是標(biāo)準(zhǔn)錯(cuò)誤輸出流對(duì)象(stderr),輸出到屏幕上。
fprintf是C/C++中的一個(gè)格式化寫—庫函數(shù),位于頭文件中,其作用是格式化輸出到一個(gè)流/文件中;(重點(diǎn)是流/文件)
printf()函數(shù)是格式化輸出函數(shù), 一般用于向標(biāo)準(zhǔn)輸出設(shè)備按規(guī)定格式輸出(重點(diǎn)是標(biāo)準(zhǔn)輸出設(shè)備,有時(shí)候輸出的不一定顯示在屏幕上,只是編譯器規(guī)定顯示到屏幕上而已。)
總結(jié):也就是說printf()其實(shí)不是輸出屏幕上的,只是這個(gè)標(biāo)準(zhǔn)輸出設(shè)備中,編譯器規(guī)定顯示到屏幕上而已,而真正輸出到屏幕是fprintf(stderr,"cdw");其中stderr就是輸出到屏幕上的流。它也可以 fprintf( FILE *stream, const char *format,...),這個(gè)就是輸出到文件流中的。
比如:一般情況下,你這兩個(gè)語句運(yùn)行的結(jié)果是相同的,沒有區(qū)別,只有一下情況才有區(qū)別:運(yùn)行你的程序的時(shí)候,命令行上把輸出結(jié)果進(jìn)行的轉(zhuǎn)向,比如使用下面的命令把你的程序a.c運(yùn)行的結(jié)果轉(zhuǎn)向到記事本文件a.txt:a.exe > a.txt
在這樣的情況,如果使用printf輸出錯(cuò)誤信息,會(huì)保存到a.txt文件里面,如果使用fprintf輸出錯(cuò)誤,會(huì)顯示在屏幕上。
<4>上面中的__VA_ARGS__也是一個(gè)宏定義,表示預(yù)處理時(shí)實(shí)際的參數(shù)。如:DBG("tiaoshi.\n");
則允許的效果是 DBG(debug.c, main( ), 14): tiaoshi.內(nèi)核級(jí):
#ifdefDEBUG_S3C_MEM#define DEBUG(fmt, args...)printk(fmt, ##args)#else#define DEBUG(fmt, args...)do {} while (0)#endif
九、鏈表&狀態(tài)機(jī)與多線程(9.9.1?具體鏈表實(shí)現(xiàn)留到驅(qū)動(dòng)模塊講解)
9.1、鏈表是一個(gè)一個(gè)的節(jié)點(diǎn),每一個(gè)節(jié)點(diǎn)分為兩部分,一部分是數(shù)據(jù)區(qū)(可以由多個(gè)類型的數(shù)據(jù)),另一部分是指向下一個(gè)節(jié)點(diǎn)的指針;結(jié)構(gòu)體定義里面的變量并沒有生成,是不占空間的,相當(dāng)于聲明的作用。9.2、鏈表的數(shù)據(jù)存放在內(nèi)存的那個(gè)空間呢?(棧,不靈活,不能用date數(shù)據(jù)段)所以只能用堆內(nèi)存,申請(qǐng)一個(gè)節(jié)點(diǎn)的大小并檢測(cè)NULL, 要使用它,就得清理它,因?yàn)樯弦粋€(gè)進(jìn)程用了這段內(nèi)存,存的是臟數(shù)據(jù),然后對(duì)這個(gè)節(jié)點(diǎn)內(nèi)存賦值,鏈接起來.9.3、當(dāng)要改變頭節(jié)點(diǎn)是,也就是要給head=p賦值時(shí),必須傳 head地址即 形參(struct student *head);這樣才能真正改變,不然傳一個(gè) (struct student head)只是單純的賦值。9.4、在scanf("%d",&(s->age)) 一定要注意,studeny *s; s->age訪問的是一個(gè)變量,而不要理解成地址,所以要加&,scanf要注意&;9.5、細(xì)節(jié):<1>在 .h文件中聲明一個(gè)函數(shù)要用分號(hào),而且是英文符號(hào).用無頭節(jié)點(diǎn)的方式,需要修改頭指針位置,所以比較復(fù)雜。
<2> 定義一個(gè)node *head=NULL,想要改變head值通過函數(shù)傳參是不行的,因?yàn)閔ead是一個(gè)地址,傳參過去,只是賦值給另一個(gè)指針而已,只能修改它指向的數(shù)據(jù),而本身(地址)是不能修改的,所以要先返回修改好的地址,然后再head=node_add(head)
<3>定義、用指針都應(yīng)該想到NULL,如 node *head=NULL; node *new=(node *)mallo(sizeof(node));if(NULL!=new){ }
<4>在結(jié)構(gòu)體想定義一個(gè)字符串時(shí)不要用 char *name; 應(yīng)該要用char name[10];如果使用第一種的話,編譯通過,執(zhí)行錯(cuò)誤,因?yàn)闉閚ame賦值時(shí)就要放在代碼段中,而代碼段已確定了,所以報(bào)段錯(cuò)誤。9.6、頭節(jié)點(diǎn)、頭指針、第一個(gè)節(jié)點(diǎn):頭節(jié)點(diǎn)是一個(gè)節(jié)點(diǎn),頭節(jié)點(diǎn)的下一個(gè)指向第一個(gè)節(jié)點(diǎn),頭節(jié)點(diǎn)的數(shù)據(jù)一般存的是鏈表長度等信息,也可以是空,頭指針指向頭節(jié)點(diǎn)。鏈表可以沒有頭節(jié)點(diǎn),但不能沒有頭指針。
頭節(jié)點(diǎn)可以想成數(shù)組的0位置,其余節(jié)點(diǎn)當(dāng)作從1開始,所以有頭節(jié)點(diǎn)的長度可以定義為就是含有真實(shí)數(shù)據(jù)節(jié)點(diǎn)的個(gè)數(shù)。9.7、刪除一個(gè)節(jié)點(diǎn)應(yīng)該做的事:如果這個(gè)節(jié)點(diǎn)的數(shù)據(jù)不重要,一定要記住free()掉,你邏輯上刪除,其實(shí)仍然存在內(nèi)存中的,頭節(jié)點(diǎn)的好處就是函數(shù)返回值int可以幫助我們一些信息,而沒有頭節(jié)點(diǎn)有時(shí)必須返回head;9.8、單鏈表之逆序:見代碼。9.9、單鏈表的優(yōu)點(diǎn)和缺點(diǎn):<優(yōu)點(diǎn)>單鏈表是對(duì)數(shù)組的一個(gè)擴(kuò)展,解決了數(shù)組的大小比較死板不容易擴(kuò)展的問題。使用堆內(nèi)存來存儲(chǔ)數(shù)據(jù),將數(shù)據(jù)分散到各個(gè)節(jié)點(diǎn)之間,其各個(gè)節(jié)點(diǎn)在內(nèi)存中可以不相連,節(jié)點(diǎn)之間通過指針進(jìn)行單向鏈接。鏈表中的各個(gè)節(jié)點(diǎn)內(nèi)存不相連,有利于利用碎片化的內(nèi)存。
<缺點(diǎn)>單鏈表各個(gè)節(jié)點(diǎn)之間只由一個(gè)指針單向鏈接,這樣實(shí)現(xiàn)有一些局限性。局限性主要體現(xiàn)在單鏈表只能經(jīng)由指針單向移動(dòng)(一旦指針移動(dòng)過某個(gè)節(jié)點(diǎn)就無法再回來,如果要再次操作這個(gè)節(jié)點(diǎn)除非從頭指針開始再次遍歷一次),因此單鏈表的某些操作就比較麻煩(算法比較有局限)。
回憶之前單鏈表的所有操作(插入、刪除節(jié)點(diǎn)、 遍歷、從單鏈表中取某個(gè)節(jié)點(diǎn)的數(shù)·····),因?yàn)閱捂湵淼膯蜗蛞苿?dòng)性導(dǎo)致了不少麻煩。
總結(jié):?jiǎn)捂湵淼膯蜗蛞苿?dòng)性導(dǎo)致我們?cè)诓僮鲉捂湵頃r(shí),當(dāng)前節(jié)點(diǎn)只能向后移動(dòng)不能向前移動(dòng),因此不自由,不利于解決更復(fù)雜的算法。
9.9.1、 內(nèi)核鏈表的思想是:<1>先做一個(gè)純鏈表,沒有數(shù)據(jù)區(qū),只有節(jié)點(diǎn)的鏈接方法。然后要做一個(gè)鏈表出來,直接用純鏈表然后稍加修改就可以了。
<2>內(nèi)核中__的方法不要輕易使用,是給內(nèi)核用的,否則容易出錯(cuò),用戶應(yīng)該使用沒有__的方法;如:__list_add() ; list_add();
<3>內(nèi)核默認(rèn)是頭指針+頭節(jié)點(diǎn)的思路。
<4>其實(shí)質(zhì)就是操作里面內(nèi)嵌 純鏈表這個(gè)變量,再利用controf宏來訪問結(jié)構(gòu)體的數(shù)據(jù)。詳情見驅(qū)動(dòng)。9.9.2、狀態(tài)機(jī):
<1>概念:其實(shí)就是有多種狀態(tài)切換,如電腦的休眠、關(guān)機(jī)、睡眠。
<2>類型:(1)Moore型狀態(tài)機(jī)特點(diǎn)是:輸出只與當(dāng)前狀態(tài)有關(guān)(與輸入信號(hào)無關(guān))。相對(duì)簡(jiǎn)單,考慮狀態(tài)機(jī)的下一個(gè)狀態(tài)時(shí)只需要考慮它的當(dāng)前狀態(tài)就行了。
(2)Mealy型狀態(tài)機(jī)的特點(diǎn)是:輸出不只和當(dāng)前狀態(tài)有關(guān),還與輸入信號(hào)有關(guān)。狀態(tài)機(jī)接收到一個(gè)輸入信號(hào)需要跳轉(zhuǎn)到下一個(gè)狀態(tài)時(shí),狀態(tài)機(jī)綜合考慮2個(gè)條件(當(dāng)前狀態(tài)、輸入值)后才決定跳轉(zhuǎn)到哪個(gè)狀態(tài)。
<3>理解:要時(shí)時(shí)刻刻檢查當(dāng)前狀態(tài),用循環(huán)+switch(狀態(tài));然后根據(jù)輸入信號(hào),進(jìn)行更多的處理,轉(zhuǎn)換到其他狀態(tài)。
十、增補(bǔ)知識(shí)
10.1、一個(gè)字節(jié)可以表示8位字符,字符真的有256種,128~255表示西歐字符,是不常見,詳情見文檔。字符相加的時(shí)候,會(huì)自動(dòng)轉(zhuǎn)成 int型加。10.2、在C中,默認(rèn)的基礎(chǔ)數(shù)據(jù)類型均為signed,現(xiàn)在我們以char為例,說明(signed) char與unsigned char之間的區(qū)別。
首先在內(nèi)存中,char與unsigned char沒有什么不同,都是一個(gè)字節(jié),唯一的區(qū)別是,char的最高位為符號(hào)位,因此char能表示-127~127,unsigned char沒有符號(hào)位,因此能表示0~255,這個(gè)好理解,8個(gè)bit,最多256種情況,因此無論如何都能表示256個(gè)數(shù)字。10.3、為什么在鏈接時(shí)需要一個(gè)鏈接地址?因?yàn)閿?shù)據(jù)是要放在一個(gè)模擬地址內(nèi)存空間的,它要把這個(gè)數(shù)據(jù)先加載到寄存器,才能給cpu使用,那么寄存器怎么知道是哪個(gè)內(nèi)存地址位置呢,是因?yàn)樵诰幾g時(shí),編譯出像 ldr r0 0x12345678 ,而這個(gè)0x12345678就是內(nèi)存地址,再編譯出像 ldr r1,[r0] ,這樣就可以拿到0x12345678內(nèi)存位置的數(shù)據(jù)了10.4、printf 變參?10.5、arm-2009q3.tar.bz2 這套編譯器自帶了函數(shù)庫,比如有strcmp , malloc ,printf 等,但是有些庫函數(shù)我們卻不能用他們,比如printf,因?yàn)檫@個(gè)函數(shù)默認(rèn)是同過屏幕輸出的,而我們常用uart調(diào)試。感覺malloc也不能用,因?yàn)槲覀儾恢纼?nèi)存哪一塊做了堆內(nèi)存,只有系統(tǒng)才知道。10.6、清bss段:編譯器可能已經(jīng)幫我們做了,只是在重定位那節(jié),因?yàn)橐囟ㄎ荒遣糠謨?nèi)存空間并沒有清0 ,所以要手動(dòng)編程清bss段。
-
芯片
+關(guān)注
關(guān)注
458文章
51419瀏覽量
428701 -
內(nèi)存
+關(guān)注
關(guān)注
8文章
3074瀏覽量
74460 -
編譯
+關(guān)注
關(guān)注
0文章
663瀏覽量
33122
原文標(biāo)題:C語言高級(jí)部分總結(jié)
文章出處:【微信號(hào):mcu168,微信公眾號(hào):硬件攻城獅】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
MCU編程語言和開發(fā)環(huán)境介紹
C語言與Java語言的對(duì)比
C語言與其他編程語言的比較
Orin芯片的編程語言支持
hex文件如何查看原c語言代碼
技術(shù)干貨驛站 ▏深入理解C語言:掌握程序結(jié)構(gòu)知識(shí)

C++語言基礎(chǔ)知識(shí)
按照這樣學(xué)習(xí)C語言,成為卷王不是夢(mèng)!

PLC編程語言和C語言的區(qū)別
C語言基礎(chǔ)-為什么要使用C?
C語言#define的應(yīng)用

評(píng)論