国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

C/C++

microcosm1994 / 2079人閱讀

摘要:另外棧內存出了作用域就會自動釋放掉,所以不需要手動去回收的。,其中指針變量的聲明有如下三種形式其中第一種是被推薦的寫法。

數據類型

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;//用來記錄總格式化字符的總個數,因為需要給結果字串最后位置添加一個"