摘要:另外棧內(nèi)存出了作用域就會(huì)自動(dòng)釋放掉,所以不需要手動(dòng)去回收的。,其中指針變量的聲明有如下三種形式其中第一種是被推薦的寫(xiě)法。
數(shù)據(jù)類(lèi)型C語(yǔ)言中的基本數(shù)據(jù)類(lèi)型,對(duì)于它分為兩種:
1、signed 有符號(hào)的類(lèi)型,也就是支持正負(fù)號(hào)的。
2、unsigned 無(wú)符號(hào)的類(lèi)型,也就是沒(méi)有負(fù)號(hào),取值從0開(kāi)始。
有符號(hào)和無(wú)符號(hào)的數(shù)據(jù)類(lèi)型有啥區(qū)別呢?其實(shí)就是取值范圍不一樣,下面看一張對(duì)照表:
C中的基本整形數(shù)據(jù)類(lèi)型為:int 、short、long、char。其中發(fā)現(xiàn)上面int 和 long在C中占的字節(jié)數(shù)是一樣的,都是占4個(gè)字節(jié),這個(gè)有別于java,在java中l(wèi)ong是占8個(gè)字節(jié)嘛,下面可以用sizeof()來(lái)打印一下其類(lèi)型的長(zhǎng)度:
對(duì)于這個(gè)其實(shí)是隨編譯器而異的,下面來(lái)總結(jié)一下不同編譯器下的基本數(shù)據(jù)類(lèi)型所占的字節(jié)數(shù):
16位編譯器
char :1個(gè)字節(jié)
char*(即指針變量): 2個(gè)字節(jié)
short int : 2個(gè)字節(jié)
int: 2個(gè)字節(jié)
unsigned int : 2個(gè)字節(jié)
float: 4個(gè)字節(jié)
double: 8個(gè)字節(jié)
long: 4個(gè)字節(jié)
long long: 8個(gè)字節(jié)
unsigned long: 4個(gè)字節(jié)
32位編譯器
char :1個(gè)字節(jié)
char*(即指針變量): 4個(gè)字節(jié)(32位的尋址空間是2^32, 即32個(gè)bit,也就是4個(gè)字節(jié)。同理64位編譯器)
short int : 2個(gè)字節(jié)
int: 4個(gè)字節(jié)
unsigned int : 4個(gè)字節(jié)
float: 4個(gè)字節(jié)
double: 8個(gè)字節(jié)
long: 4個(gè)字節(jié)
long long: 8個(gè)字節(jié)
unsigned long: 4個(gè)字節(jié)
64位編譯器
char :1個(gè)字節(jié)
char*(即指針變量): 8個(gè)字節(jié)
short int : 2個(gè)字節(jié)
int: 4個(gè)字節(jié)
unsigned int : 4個(gè)字節(jié)
float: 4個(gè)字節(jié)
double: 8個(gè)字節(jié)
long: 8個(gè)字節(jié)
long long: 8個(gè)字節(jié)
unsigned long: 8個(gè)字節(jié)
其實(shí)long int = long;在標(biāo)準(zhǔn)中規(guī)定int至少要和short一樣長(zhǎng),long至少要和int一樣長(zhǎng)。
在實(shí)際中可能會(huì)用一個(gè)更加清晰的數(shù)據(jù)類(lèi)型,如:
其實(shí)用的就是定義好的宏
這種寫(xiě)法是被推薦的,因?yàn)闀?huì)比較清晰。
基數(shù)數(shù)據(jù)類(lèi)型除了上面的整型之外,還有浮點(diǎn)型,具體如下表:
另外需要注意:在C中并沒(méi)有專(zhuān)門(mén)的boolean類(lèi)型,而是:非0既true、非null為true;
輸出格式化必須要寫(xiě)一個(gè)格式化占位符參數(shù),其實(shí)跟java中的String.format()的用法類(lèi)似,如:
而其中的“%d”表示輸出整型變量,那對(duì)于其它數(shù)據(jù)類(lèi)型其輸出占位符又如何寫(xiě)呢,其它之前的表格中已經(jīng)有說(shuō)明,如下:
雖說(shuō)"%d"可以輸出所有的整型,但是還是用上圖中對(duì)應(yīng)的輸出會(huì)更加精準(zhǔn)。
另外sprintf()這個(gè)函數(shù)在實(shí)際當(dāng)中也非常常用,比如要打印某個(gè)目錄下的按規(guī)律生成的文件,比如:
也就是將2、3參數(shù)格式化的字符復(fù)制到str當(dāng)中。
數(shù)組與內(nèi)存布局在C中聲明數(shù)組必須指定長(zhǎng)度,或者聲明與賦值寫(xiě)在一起
另外它是在棧上分配內(nèi)存的,而棧上的內(nèi)存是有限制的,在mac上可以使用“ulimit -a”來(lái)查看其最大棧內(nèi)存:
也就是最大棧的大小是8192K,但是需要注意:并不是我們程序也能申請(qǐng)這么大的棧內(nèi)存的,因?yàn)橄癯绦虻囊粋€(gè)函數(shù)參數(shù),返回值等也是存放在棧中的。另外棧內(nèi)存出了作用域就會(huì)自動(dòng)釋放掉,所以不需要手動(dòng)去回收的。
前面說(shuō)了棧大小不是特別大,那如果對(duì)于要的內(nèi)存超過(guò)棧大小的該怎么辦呢,當(dāng)然就是在堆中進(jìn)行申請(qǐng)嘍,此時(shí)就存在以下幾種堆中申請(qǐng)內(nèi)存的一些函數(shù),下面來(lái)說(shuō)明下:
malloc:在堆中申請(qǐng)內(nèi)存但不會(huì)對(duì)其申請(qǐng)的內(nèi)存進(jìn)行初始化,如在堆中申請(qǐng)1MB的內(nèi)存:
另外還需要注意:由于申請(qǐng)的內(nèi)存還沒(méi)初始化,所以一般在malloc申請(qǐng)內(nèi)存之后會(huì)使用memset保存其申請(qǐng)的內(nèi)存是一片純白的,而不是用了之前的臟數(shù)據(jù),因?yàn)樯暾?qǐng)內(nèi)存有可能會(huì)重用之前的內(nèi)存,具體用法如下:
還有一點(diǎn)需要注意:堆中申請(qǐng)的內(nèi)存是不會(huì)自動(dòng)釋放的,需要手動(dòng)去釋放,如下:
calloc():申請(qǐng)內(nèi)存并將內(nèi)存初始化為null,具體用法:
其實(shí)它就等價(jià)于:
realloc():重新對(duì)malloc申請(qǐng)的內(nèi)存大小進(jìn)行調(diào)整,如下:
那什么場(chǎng)景會(huì)用到它呢,這里舉一個(gè)TCP傳輸粘包問(wèn)題,比如發(fā)送“1,2,3,4,5,6”數(shù)據(jù),而接收的時(shí)候可能分幾次才能接收完,比如是先接收到了“1,2,3”,之后再接收到了“4,5”,最后接收了“6”,至此才將數(shù)據(jù)接收完,那此時(shí)的緩沖區(qū)char首先申請(qǐng)的是3個(gè)字節(jié),于是乎“1、2、3”剛好接收滿了,但此時(shí)還不是一個(gè)完整的數(shù)據(jù)包,所以還得接著等“4,5,6”,當(dāng)接收到了“4、5”了,就需要對(duì)緩沖區(qū)進(jìn)行擴(kuò)容用以存放這兩個(gè)字節(jié)了,同樣的最后接收到了"6",則繼續(xù)再要對(duì)緩存沖再擴(kuò)容一個(gè)字節(jié)。 當(dāng)然直接申請(qǐng)一個(gè)足夠大的緩存區(qū)不就不用擴(kuò)容了么,這是因?yàn)閿?shù)據(jù)包的大小是無(wú)法確定的,這里只是為了說(shuō)明問(wèn)題舉了個(gè)簡(jiǎn)單的粟子而已。
alloca():向棧中申請(qǐng)內(nèi)存。用法如下:
內(nèi)存布局
物理內(nèi)存:通過(guò)物理內(nèi)存條獲得的內(nèi)存空間。
虛擬內(nèi)存:它是一種內(nèi)存管理技術(shù),能夠均處一部分硬盤(pán)空間充當(dāng)內(nèi)存使用。
而在C當(dāng)中的內(nèi)存布局如下:
其中最頂部的是內(nèi)核空間:
除這個(gè)內(nèi)核空間之外的則是用戶(hù)進(jìn)程的內(nèi)存空間:
下面看一下有哪些內(nèi)容,首先是棧區(qū):
接著是內(nèi)存映射段:
接著就是堆區(qū)了:
接著就是BSS段了:
接著再就是數(shù)據(jù)段:
最后一個(gè)則是文本段:
咱們基于上面的來(lái)畫(huà)一個(gè)簡(jiǎn)化版本:
其中“預(yù)留區(qū)”是程序看不見(jiàn)的區(qū)域,系統(tǒng)預(yù)留滴。
這里來(lái)對(duì)堆內(nèi)存地址由低往高進(jìn)行說(shuō)明:在堆區(qū)申請(qǐng)內(nèi)存是調(diào)用了glibc(C的標(biāo)準(zhǔn)庫(kù)、運(yùn)行庫(kù),類(lèi)似于java的JDK)提供的malloc方法,而它的底層是由Linux的brk和mmap兩種方式來(lái)實(shí)現(xiàn)的,而其中:brk申請(qǐng)內(nèi)存的方式是將內(nèi)存指針(假設(shè)為_(kāi)edata)往高地址堆,目前_edata指向堆內(nèi)存的起始位置 :
假如申請(qǐng)10K的內(nèi)存,此時(shí)就會(huì)將_edata由低地址往上推10K的大小,如下:
如果再申請(qǐng)一個(gè)10K,同樣的往上再推10K,如下:
那如果A被釋放掉了,會(huì)發(fā)生什么情況呢");
那此時(shí)如果再申請(qǐng)一個(gè)10K的內(nèi)存,發(fā)現(xiàn)A這個(gè)空間剛好滿足則會(huì)重用它,_edata并不會(huì)往上再去開(kāi)辟新內(nèi)存空間,那假如申請(qǐng)的內(nèi)存大于10K,比如11K,此時(shí)A這個(gè)區(qū)域內(nèi)存滿足不了要申請(qǐng)的11K大小,所以還是會(huì)往上推11K大小的內(nèi)存,如下:
那brk方式申請(qǐng)的內(nèi)存就永遠(yuǎn)不會(huì)收縮么,其實(shí)不是這樣的,像這種場(chǎng)景就會(huì):此時(shí)C被釋放了,內(nèi)存就會(huì)收縮了,如下:
而對(duì)于mmap申請(qǐng)內(nèi)存的方式為:找一塊滿足大小的內(nèi)存既可,而不會(huì)像brk方式往上今次推指針,所以它的內(nèi)存隨時(shí)都可以被釋放的,那什么時(shí)候用brk,什么時(shí)候用mmap呢?其實(shí)是要申請(qǐng)的堆內(nèi)存小于128k則用brk方式申請(qǐng),否則用mmap申請(qǐng),注意:此128K是個(gè)閾值,是可以人為配置的。
好,明白了上面的之后,回到咱們開(kāi)篇所指出的問(wèn)題:為啥在malloc動(dòng)態(tài)申請(qǐng)內(nèi)存之后,需要用memset手動(dòng)再去給內(nèi)存進(jìn)行一個(gè)初始化?因?yàn)閎rk方式有可能會(huì)存在復(fù)用之前申請(qǐng)過(guò)的內(nèi)存,如果不初始化有可能該內(nèi)存是之前申請(qǐng)過(guò)的,這樣就會(huì)造成一些數(shù)據(jù)的混亂。
那對(duì)于malloc底層為啥不全采用mmap方式來(lái)實(shí)現(xiàn)呢?因?yàn)閙map效率明顯不如brk推指針的方式,所以就存在于兩種方式來(lái)實(shí)現(xiàn)了。
另外對(duì)于數(shù)組而言其實(shí)是一段連續(xù)的內(nèi)存地址,如下:
頭文件基礎(chǔ)知識(shí)
我們知道對(duì)于C、C++的代碼通常是有.h頭文件和.c&.cpp的源文件的,如下:
那么在.h頭文件中能否有具體實(shí)現(xiàn)呢?答案是肯定的,下面來(lái)試驗(yàn)一下:
另外對(duì)于要使用指定頭文件是需要用include來(lái)將其包含進(jìn)來(lái)的,類(lèi)似于java中的import,如下:
但是!跟java中的import是有區(qū)別的,在java中是不能夠傳遞import的,怎么理解,看下面java代碼:
而ArrayList里面是import了它了:
那如果我們?cè)趍ain中也想用Consumer這個(gè)類(lèi)的話,還需要再導(dǎo)一遍,如下:
也就是說(shuō):雖然ArrayList已經(jīng)import過(guò)了Consumer,而我們?cè)趍ain中也已經(jīng)import了ArrayList,但是Consumer并不會(huì)被傳遞到main方法中,使用時(shí)是需要再次導(dǎo)入的,但是!C中是可以傳遞include的,下面用代碼來(lái)說(shuō)明一下:
然后在main.h中去include我們新建的這個(gè)頭文件:
那我們?cè)趍ain.c中能否去調(diào)用a頭文件中聲明的test3()函數(shù)呢,當(dāng)然能:
那思考一下為啥C、C++要分一個(gè)頭文件和源文件,而不像Java只有一個(gè)源文件呢?其實(shí).h就是將行為給暴露,其具體實(shí)現(xiàn)不暴露,當(dāng)然如果想暴露具體實(shí)現(xiàn)那可以在.h中去用具體的方法來(lái)暴露,如:
而通常的只定義了函數(shù)的聲明,如:
這樣當(dāng)別人想使用該函數(shù)時(shí)只需要include頭文件既可,具體的實(shí)現(xiàn)細(xì)節(jié)則不會(huì)暴露給調(diào)用者。
指針“指針是一個(gè)變量,它的值是一個(gè)地址?!保渲兄羔樧兞康穆暶饔腥缦氯N形式:
其中第一種是被推薦的寫(xiě)法。
其中還需要注意:在聲明指針時(shí)如果未賦值,則是一個(gè)野指針【也就是有可能指向了一個(gè)不能被使用的地址從而造成程序的錯(cuò)誤】,所以在聲明時(shí)一定要賦值,如下:
那如果想取變量的地址則可以用“&”符,如下:
那如果想獲取指針指向變量地址的值則需要用“*”解引用的操作,如下:
下面來(lái)看一下p指針占用了幾個(gè)字節(jié):
需要注意的是:由于目前是在64位系統(tǒng)上運(yùn)行的,所以是8個(gè)字節(jié),如果是在32位運(yùn)行則長(zhǎng)度是4個(gè)字。
有了指針之后就可以用它去操縱內(nèi)存,下面來(lái)通過(guò)指針的形式來(lái)修改變量的值,如下:
指針是可以進(jìn)行++、--操作的,比如用指針來(lái)遍歷數(shù)組,下面來(lái)看下:
其中“array_p1++”是先取了值,然后再對(duì)其指針進(jìn)行++,如果是寫(xiě)成"++array_p1",則是先對(duì)指針進(jìn)行加加,然后再取值,最終輸出就會(huì)漏掉一個(gè),如下:
其中還有一種直接通過(guò)數(shù)組來(lái)進(jìn)行相加也能達(dá)到遍歷的目的,如下:
要取其數(shù)組的內(nèi)容則需要解引用:
另外還有一個(gè)細(xì)節(jié):為啥數(shù)組取地址時(shí)木有加“&”符號(hào):
這是因?yàn)樵贑中數(shù)組名就是數(shù)組的首地址,下面來(lái)看下:
下面有個(gè)概念需要弄清楚:“數(shù)組指針”和“指針數(shù)組”,這個(gè)在面試可能會(huì)經(jīng)常變問(wèn)到,下面來(lái)看下:
首先肯定得要將數(shù)組的指針+1,來(lái)定位到第二維的數(shù)組,所以array_p2+1,然后再取出它的值則是*(array_p2+1),接著這個(gè)值是一個(gè)數(shù)組,所以還得數(shù)組名+1來(lái)將指針移到要輸出的第二個(gè)元素上來(lái),所以此時(shí)為*(array_p2+1)+1,最后再解引用取出指針的值,所以整個(gè)的式子如:((array_p2+1)+1),下面來(lái)驗(yàn)證一下:
接下來(lái)更繞的來(lái)了,先把代碼寫(xiě)出來(lái):
先記著這個(gè)原則:“從右往左看 const 修飾誰(shuí) 誰(shuí)就不可變”:
意味著不能通過(guò)p2來(lái)修改tem的值,如下:
因?yàn)閏onst是修飾的char,而非p2變量,所以p2的內(nèi)容可以被更改,如下:
繼續(xù)來(lái)理解下一個(gè):
這個(gè)跟上一個(gè)效果是一模一樣的,為啥?因?yàn)閏onst只能修飾char,不能修飾*。
繼續(xù)看下一個(gè):
還是按照從右往左的原則,const這次修飾的是變量p4,也就是說(shuō)p4的內(nèi)容是不允許修改的,如下:
但是可以通過(guò)指針修改指向地址的值,如下:
下面兩個(gè)是啥都不能變了,如下:
拿p5舉例,既不能修改p5指針的值,如下:
下面再來(lái)看一個(gè)跟指針相關(guān)的東東---多級(jí)指針:
解引用則為:
函數(shù) 函數(shù)聲明
C中的函數(shù)跟Java的方法基本類(lèi)似,但是在C中的函數(shù)需要注意:我們使用的函數(shù)必須在之前聲明,否則會(huì)編譯不過(guò),如下:
可以在之前做一個(gè)聲明既可:
所以一般函數(shù)都聲明在頭文件中,然后一.c文件中頭部進(jìn)行include,這樣就如同上面的聲明一樣了。
函數(shù)傳參傳值:把參數(shù)的值給函數(shù),如下:
也就是說(shuō)不會(huì)改變?cè)凶兞康闹怠?/p>
傳引用:
也就是可以通過(guò)指針來(lái)修改原值,有了這個(gè)特性,那么多級(jí)指針就變得非常有意義了,如下:
可變參數(shù):
在Java中我們知道可變參數(shù)是由...來(lái)弄的,其實(shí)在C中也類(lèi)似,其中我們經(jīng)常打印的printf()函數(shù)就接收一個(gè)可變參數(shù),查看一下源碼便知:
所以咱們也來(lái)弄一個(gè)可變參數(shù):
參數(shù)中不能只有可變參數(shù),必須要有一個(gè)確定參數(shù),所以修改如下:
接著問(wèn)題來(lái)了,如何來(lái)取出可變參數(shù)的值呢?看下面:
然后接著進(jìn)行遍歷,根據(jù)類(lèi)型:
注意:其確定參數(shù)給NULL值是可以的,反正是要有一個(gè),什么類(lèi)型的都可以,不能沒(méi)有確參,如下:
函數(shù)指針
定義:指向函數(shù)的指針。
其中"void (p) (char)"就是一個(gè)函數(shù)指針,void表示該函數(shù)無(wú)返回值;(char*)表示函數(shù)的參數(shù)列表,目前只接收一個(gè)參數(shù);(*p)表示指向函數(shù)的指針。
其實(shí)也就相當(dāng)于Java中的方法回調(diào)的意思,另外可以將函數(shù)的聲明定義成一個(gè)typedef,如下:
可以用函數(shù)指針模擬HTTP請(qǐng)求,如果成功就執(zhí)行某個(gè)函數(shù),失敗則執(zhí)行某個(gè)函數(shù),如下:
預(yù)處理器
預(yù)處理器主要是完成文本替換的,常用的預(yù)處理器如下:
#include:這個(gè)就不多說(shuō)了。
#if、#elif、#else、#endif:在實(shí)際代碼編寫(xiě)中會(huì)遇到這樣的寫(xiě)法,如下:
假如不想要這段代碼了,則直接更改條件既可:
適用的場(chǎng)合就是假如寫(xiě)的代碼不想要了,則不用注釋掉了。
#define、#ifdef、#ifndef:這里可以配合#define的宏定義來(lái)配合上面的一些條件來(lái)使用,如下:
其中定義的宏是可以被取消的,如下:
其中#define宏定義分為兩種:宏變量和宏函數(shù),具體如下:
這樣在代碼中就可以使用I來(lái)表示1了,如下:
而在之前說(shuō)過(guò)預(yù)處理其實(shí)也就是做文本替換用的,所以代碼中所有的I就會(huì)被預(yù)處理器替換為1。
接下來(lái)看一下宏函數(shù):
此時(shí)就可以在代碼中進(jìn)行調(diào)用了,如下:
但是宏函數(shù)也有陷阱需要注意,看下面這個(gè):
如果修改一下:
期望的結(jié)果應(yīng)該是(1 + 10)* (10 + 10) = 220,但是運(yùn)行看:
居然變成了:1 + 10 * 10 + 10了,所以需要特別注意,可以加個(gè)括號(hào)解決:
下面來(lái)看一下宏函數(shù)有哪些優(yōu)缺點(diǎn): 優(yōu)點(diǎn):它只是文本替換,使用到宏函數(shù)的地方會(huì)執(zhí)行替換,不會(huì)有函數(shù)調(diào)用的開(kāi)銷(xiāo)(將參數(shù)壓棧,釋放棧之類(lèi)的)。 缺點(diǎn):1、不會(huì)對(duì)我們的代碼執(zhí)行檢查,不像普通的函數(shù)在編寫(xiě)階段就會(huì)給出相印的錯(cuò)誤提示。2、假如宏函數(shù)是一個(gè)非常復(fù)雜的函數(shù),那么每個(gè)調(diào)用它的地方就會(huì)完全替換,造成代碼冗余使得最終生成的目標(biāo)文件(如so)增大了,比如:
如果代碼中調(diào)了兩次它,如下:
實(shí)際上文本替換之后就是:
其實(shí)內(nèi)聯(lián)函數(shù)跟宏函數(shù)的執(zhí)行模式是一樣的,也是執(zhí)行代碼替換,但不是一個(gè)概念,內(nèi)聯(lián)函數(shù)在編寫(xiě)時(shí)會(huì)做檢查,另外它里面的代碼不能編寫(xiě)過(guò)于復(fù)雜的代碼,如使用了switch、while等復(fù)雜控制邏輯,否則會(huì)將內(nèi)聯(lián)函數(shù)降級(jí)為普通函數(shù),那何為內(nèi)聯(lián)函數(shù)呢?其實(shí)就是inline關(guān)鍵字,如下:
#pragma:這個(gè)用得較少,在VS中在頭文件中會(huì)自動(dòng)有一個(gè)如下東東:
它表示該頭文件只能被引用一次,其實(shí)通用的寫(xiě)法是用它:
其效果都是一樣的。
自己實(shí)現(xiàn)sprintf功能
自己實(shí)現(xiàn)一個(gè)只考慮傳整型參數(shù)的情況就成,那如何來(lái)實(shí)現(xiàn)呢?下面開(kāi)始:
如果遇到了“%”,則需要判斷一下它的下一位字符是否是“d”字符,只有這樣才是一個(gè)合法的占位,所以:
然后如果發(fā)現(xiàn)此參數(shù)是一個(gè)負(fù)數(shù),則需要前面手動(dòng)加一個(gè)“-”,如下:
然后再將解析到的字符串參數(shù)遍歷到結(jié)果串當(dāng)中,如下:
下面使用一下咱們自己編寫(xiě)的函數(shù)看下效果:
原來(lái)是少了這么一句關(guān)鍵邏輯,如下:
//
// Created by xiongwei on 2018/9/23.
//
#ifndef LSN3_EXAMPLE_MYSPRINTF_H
#define LSN3_EXAMPLE_MYSPRINTF_H
#include //用來(lái)獲取可變參數(shù)
void mysprintf(char *buffer, const char *fmt, ...) {
//首先聲明va_list
va_list arg_list;
va_start(arg_list, buffer);
char *b = buffer;
int count = 0;//用來(lái)記錄總格式化字符的總個(gè)數(shù),因?yàn)樾枰o結(jié)果字串最后位置添加一個(gè)"