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