操作系統(tǒng)中的棧和堆
注:這里所說的堆和棧與數(shù)據(jù)結(jié)構(gòu)中的堆和棧不是一回事。
我們先來看看一個由C/C++/OBJC編譯的程序占用內(nèi)存分布的結(jié)構(gòu):-
棧區(qū)(stack):由系統(tǒng)自動分配,一般存放函數(shù)參數(shù)值叉谜、局部變量的值等。由編譯器自動創(chuàng)建與釋放踩萎。其操作方式類似于數(shù)據(jù)結(jié)構(gòu)中的棧停局,即后進先出、先進后出的原則香府。
- 例如:在函數(shù)中申明一個局部變量
int b
, 系統(tǒng)自動在棧中為b開辟空間董栽。
- 例如:在函數(shù)中申明一個局部變量
-
堆區(qū)(heap):一般由程序員申請并指明大小,最終也由程序員釋放企孩。如果程序員不釋放锭碳,程序結(jié)束時可能會由OS回收。對于堆區(qū)的管理是采用鏈表式管理的勿璃,操作系統(tǒng)有一個記錄空閑內(nèi)存地址的鏈表擒抛,當(dāng)接收到程序分配內(nèi)存的申請時,操作系統(tǒng)就會遍歷該鏈表补疑,遍歷到一個記錄的內(nèi)存地址大于申請內(nèi)存的鏈表節(jié)點歧沪,并將該節(jié)點從該鏈表中刪除,然后將該節(jié)點記錄的內(nèi)存地址分配給程序莲组。
- 例如:在C中malloc函數(shù)
char *p1;
p1 = (char *)malloc(10);
但是p1本身是在棧中的诊胞。
- 例如:在C中malloc函數(shù)
-
鏈表:是一種常見的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),一般分為單向鏈表胁编、雙向鏈表厢钧、循環(huán)鏈表。以下為單向鏈表的結(jié)構(gòu)圖:
單向鏈表是鏈表中最簡單的一種嬉橙,它包含兩個區(qū)域早直,一個信息域和一個指針域。信息域保存或顯示關(guān)于節(jié)點的信息市框,指針域儲存下一個節(jié)點的地址霞扬。
上述的空閑內(nèi)存地址鏈表的信息域保存的就是空閑內(nèi)存的地址。 全局區(qū)/靜態(tài)區(qū):顧名思義枫振,全局變量和靜態(tài)變量存儲在這個區(qū)域喻圃。只不過初始化的全局變量和靜態(tài)變量存儲在一塊,未初始化的全局變量和靜態(tài)變量存儲在一塊粪滤。程序結(jié)束后由系統(tǒng)釋放斧拍。
文字常量區(qū):這個區(qū)域主要存儲字符串常量。程序結(jié)束后由系統(tǒng)釋放杖小。
程序代碼區(qū):這個區(qū)域主要存放函數(shù)體的二進制代碼肆汹。
下面舉一個前輩寫的例子:
//main.cpp
int a = 0; // 全局初始化區(qū)
char *p1; // 全局未初始化區(qū)
main {
int b; // 棧
char s[] = "abc"; // 棧
char *p2; // 棧
char *p3 = "123456"; // 123456\\0在常量區(qū),p3在棧上
static int c =0予权; // 全局靜態(tài)初始化區(qū)
p1 = (char *)malloc(10);
p2 = (char *)malloc(20); // 分配得來的10和20字節(jié)的區(qū)域就在堆區(qū)
strcpy(p1, "123456"); // 123456\\0在常量區(qū)昂勉,這個函數(shù)的作用是將"123456" 這串字符串復(fù)制一份放在p1申請的10個字節(jié)的堆區(qū)域中。
// p3指向的"123456"與這里的"123456"可能會被編譯器優(yōu)化成一個地址扫腺。}
strcpy函數(shù)原型聲明:extern char *strcpy(char* dest, const char *src);
功能:把從src地址開始且含有NULL結(jié)束符的字符串復(fù)制到以dest開始的地址空間岗照。
結(jié)構(gòu)體(Struct)
在C語言中,結(jié)構(gòu)體(struct)指的是一種數(shù)據(jù)結(jié)構(gòu)笆环。結(jié)構(gòu)體可以被聲明為變量攒至、指針或數(shù)組等,用以實現(xiàn)較復(fù)雜的數(shù)據(jù)結(jié)構(gòu)躁劣。結(jié)構(gòu)體同時也是一些元素的集合嗓袱,這些元素稱為結(jié)構(gòu)體的成員(member),且這些成員可以為不同的類型习绢,成員一般用名字訪問渠抹。我們來看看結(jié)構(gòu)體的定義:
struct tag { member-list } variable-list;
- struct:結(jié)構(gòu)體關(guān)鍵字。
- tag:結(jié)構(gòu)體標(biāo)簽闪萄。
- member-list:結(jié)構(gòu)體成員列表梧却。
- variable-list:為結(jié)構(gòu)體聲明的變量列表。
在一般情況下败去,tag
放航,member-list
,variable-list
這三部分至少要出現(xiàn)兩個圆裕。以下為示例:
// 該結(jié)構(gòu)體擁有3個成員广鳍,整型的a荆几,字符型的b,雙精度型的c
// 并且為該結(jié)構(gòu)體聲明了一個變量s1
// 該結(jié)構(gòu)體沒有標(biāo)明其標(biāo)簽
struct{
int a;
char b;
double c;
} s1;
// 該結(jié)構(gòu)體擁有同樣的三個成員
// 并且該結(jié)構(gòu)體標(biāo)明了標(biāo)簽EXAMPLE
// 該結(jié)構(gòu)體沒有聲明變量
struct EXAMPLE{
int a;
char b;
double c;
};
//用EXAMPLE標(biāo)簽的結(jié)構(gòu)體赊时,另外聲明了變量t1吨铸、t2、t3
struct EXAMPLE t1, t2[20], *t3;
以上就是簡單結(jié)構(gòu)體的代碼示例祖秒。結(jié)構(gòu)體的成員可以包含其他結(jié)構(gòu)體诞吱,也可以包含指向自己結(jié)構(gòu)體類型的指針。結(jié)構(gòu)體的變量也可以是指針竭缝。
下面我們來看看結(jié)構(gòu)體成員的訪問房维。結(jié)構(gòu)體成員依據(jù)結(jié)構(gòu)體變量類型的不同,一般有2種訪問方式抬纸,一種為直接訪問咙俩,一種為間接訪問。直接訪問應(yīng)用于普通的結(jié)構(gòu)體變量湿故,間接訪問應(yīng)用于指向結(jié)構(gòu)體變量的指針暴浦。直接訪問使用結(jié)構(gòu)體變量名.成員名,間接訪問使用(*結(jié)構(gòu)體指針名).成員名或者使用結(jié)構(gòu)體指針名->成員名晓锻。相同的成員名稱依靠不同的變量前綴區(qū)分歌焦。
struct EXAMPLE{
int a;
char b;
};
//聲明結(jié)構(gòu)體變量s1和指向結(jié)構(gòu)體變量的指針s2
struct EXAMPLE s1, *s2;
//給變量s1和s2的成員賦值,注意s1.a和s2->a并不是同一成員
s1.a = 5;
s1.b = 6;
s2->a = 3;
s2->b = 4;
最后我們來看看結(jié)構(gòu)體成員存儲。在內(nèi)存中砚哆,編譯器按照成員列表順序分別為每個結(jié)構(gòu)體成員分配內(nèi)存独撇。如果想確認(rèn)結(jié)構(gòu)體占多少存儲空間,則使用關(guān)鍵字sizeof
躁锁,如果想得知結(jié)構(gòu)體的某個特定成員在結(jié)構(gòu)體的位置纷铣,則使用offsetof
宏(定義于stddef.h)。
struct EXAMPLE{
int a;
char b;
};
//獲得EXAMPLE類型結(jié)構(gòu)體所占內(nèi)存大小
int size_example = sizeof( struct EXAMPLE );
//獲得成員b相對于EXAMPLE儲存地址的偏移量
int offset_b = offsetof( struct EXAMPLE, b );
閉包(Closure)
閉包就是一個函數(shù)战转,或者一個指向函數(shù)的指針搜立,加上這個函數(shù)執(zhí)行的非局部變量。說的通俗一點槐秧,就是閉包允許一個函數(shù)訪問聲明該函數(shù)運行上下文中的變量啄踊,甚至可以訪問不同運行上文中的變量。我們用腳本語言來看一下:
function funA(callback){
alert(callback());
}
function funB(){
var str = "Hello World";
// 函數(shù)funB的局部變量刁标,函數(shù)funA的非局部變量
funA(
function(){
return str;
} )颠通;
}
通過上面的代碼我們可以看出,按常規(guī)思維來說膀懈,變量str
是函數(shù)funB
的局部變量顿锰,作用域只在函數(shù)funB
中,函數(shù)funA
是無法訪問到str
的。但是上述代碼示例中函數(shù)funA
中的callback
可以訪問到str
硼控,這是為什么呢刘陶,因為閉包性。
block原型及定義
我們來看看block的原型:
NSString * ( ^ myBlock )( int );
上面的代碼聲明了一個block(^)
原型牢撼,名字叫做myBlock
匙隔,包含一個int
型的參數(shù),返回值為NSString
類型的指針浪默。
下面來看看block
的定義:
myBlock = ^( int paramA ){
return [ NSString stringWithFormat: @"Passed number: %i", paramA ];
};
上面的代碼中牡直,將一個函數(shù)體賦值給了myBlock
變量缀匕,其接收一個名為paramA
的參數(shù)纳决,返回一個NSString
對象。
注意:一定不要忘記block后面的分號乡小。
定義好block后阔加,就可以像使用標(biāo)準(zhǔn)函數(shù)一樣使用它了:
myBlock(7);
由于block數(shù)據(jù)類型的語法會降低整個代碼的閱讀性,所以常使用typedef
來定義block類型满钟。例如胜榔,下面的代碼創(chuàng)建了GetPersonEducationInfo
和GetPersonFamilyInfo
兩個新類型,這樣我們就可以在下面的方法中使用更加有語義的數(shù)據(jù)類型湃番。
// Person.h
#import <Foundation/Foundation.h>
// Define a new type for the blocktypedef
NSString * (^GetPersonEducationInfo)(NSString *);
typedef NSString * (^GetPersonFamilyInfo)(NSString *);
@interface Person : NSObject
- (NSString *)getPersonInfoWithEducation:(GetPersonEducationInfo)educationInfo andFamily:(GetPersonFamilyInfo)familyInfo;
@end
我們用一張大師文章里的圖來總結(jié)一下block的結(jié)構(gòu):
將block作為參數(shù)傳遞
// .h
-(void) testBlock:( NSString * ( ^ )( int ) )myBlock;
// .m
-(void) testBlock:( NSString * ( ^ )( int ) )myBlock{
NSLog(@"Block returned: %@", myBlock(7) );
}
由于Objective-C是強制類型語言夭织,所以作為函數(shù)參數(shù)的block也必須要指定返回值的類型,以及相關(guān)參數(shù)類型吠撮。
閉包性
上文說過尊惰,block實際是Objc對閉包的實現(xiàn)。我們來看看下面代碼:
#import <Cocoa/Cocoa.h>
void logBlock( int ( ^ theBlock )( void ) ){
NSLog( @"Closure var X: %i", theBlock() );
}
int main( void ){
NSAutoreleasePool * pool;
int ( ^ myBlock )( void );
int x;
pool = [ [ NSAutoreleasePool alloc ] init ];
x = 42;
myBlock = ^( void ) {
return x;
};
logBlock( myBlock );
[ pool release ];
return EXIT_SUCCESS;
}
上面的代碼在main函數(shù)中聲明了一個整型泥兰,并賦值42弄屡,另外還聲明了一個block,該block會將42返回鞋诗。然后將block傳遞給logBlock
函數(shù)膀捷,該函數(shù)會顯示出返回的值42。即使是在函數(shù)logBlock
中執(zhí)行block削彬,而block又聲明在main函數(shù)中全庸,但是block仍然可以訪問到x
變量,并將這個值返回融痛。
注意:block同樣可以訪問全局變量糕篇,即使是static
。
block中變量的復(fù)制與修改
對于block外的變量引用酌心,block默認(rèn)是將其復(fù)制到其數(shù)據(jù)結(jié)構(gòu)中來實現(xiàn)訪問的拌消,如下圖:
通過block進行閉包的變量是const
的。也就是說不能在block中直接修改這些變量。來看看當(dāng)block試著增加x
的值時墩崩,會發(fā)生什么:
myBlock = ^( void ){ x++; return x;};
編譯器會報錯氓英,表明在block中變量x
是只讀的。有時候確實需要在block中處理變量鹦筹,怎么辦铝阐?別著急,我們可以用__block
關(guān)鍵字來聲明變量铐拐,這樣就可以在block中修改變量了徘键。基于之前的代碼遍蟋,給x
變量添加__block
關(guān)鍵字吹害,如下:
__block int x;
對于用__block
修飾的外部變量引用,block是復(fù)制其引用地址來實現(xiàn)訪問的虚青,如下圖:
編譯器中的block
block的數(shù)據(jù)結(jié)構(gòu)定義我們通過大師文章中的一張圖來說明:上圖這個結(jié)構(gòu)是在棧中的結(jié)構(gòu)它呀,我們來看看對應(yīng)的結(jié)構(gòu)體定義:
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
從上面代碼看出,Block_layout
就是對block結(jié)構(gòu)體的定義:
- isa指針:指向表明該block類型的類棒厘。
- flags:按bit位表示一些block的附加信息纵穿,比如判斷block類型、判斷
- block引用計數(shù)奢人、判斷block是否需要執(zhí)行輔助函數(shù)等谓媒。
- reserved:保留變量,我的理解是表示block內(nèi)部的變量數(shù)何乎。
- invoke:函數(shù)指針句惯,指向具體的block實現(xiàn)的函數(shù)調(diào)用地址。
- descriptor:block的附加描述信息宪赶,比如保留變量數(shù)宗弯、block的大小、進行copy或dispose的輔助函數(shù)指針搂妻。
- variables:因為block有閉包性蒙保,所以可以訪問block外部的局部變量。這些variables就是復(fù)制到結(jié)構(gòu)體中的外部局部變量或變量的地址欲主。
block的類型
block有幾種不同的類型邓厕,每種類型都有對應(yīng)的類,上述中isa
指針就是指向這個類扁瓢。這里列出常見的三種類型:
_NSConcreteGlobalBlock
:全局的靜態(tài)block详恼,不會訪問任何外部變量,不會涉及到任何拷貝引几,比如一個空的block昧互。例如:
#include <stdio.h>
int main(){
^{ printf("Hello, World!\\n"); } ();
return 0;
}
_NSConcreteStackBlock
:保存在棧中的block,當(dāng)函數(shù)返回時被銷毀。例如:
#include <stdio.h>
int main(){
char a = 'A';
^{ printf("%c\\n",a); } ();
return 0;
}
_NSConcreteMallocBlock
:保存在堆中的block敞掘,當(dāng)引用計數(shù)為0時被銷毀挨务。該類型的block都是由_NSConcreteStackBlock
類型的block從棧中復(fù)制到堆中形成的芒炼。例如下面代碼中癌瘾,在exampleB_addBlockToArray
方法中的block還是_NSConcreteStackBlock
類型的记劈,在exampleB
方法中就被復(fù)制到了堆中,成為_NSConcreteMallocBlock
類型的block:
void exampleB_addBlockToArray(NSMutableArray *array) {
char b = 'B';
[array addObject:^{
printf("%c\\n", b);
}];
}
void exampleB() {
NSMutableArray *array = [NSMutableArray array];
exampleB_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}
總結(jié)一下:
_NSConcreteGlobalBlock
類型的block要么是空block赫冬,要么是不訪問任何外部變量的block浓镜。它既不在棧中,也不在堆中劲厌,我理解為它可能在內(nèi)存的全局區(qū)膛薛。
_NSConcreteStackBlock
類型的block有閉包行為,也就是有訪問外部變量脊僚,并且該block只且只有有一次執(zhí)行相叁,因為棧中的空間是可重復(fù)使用的遵绰,所以當(dāng)棧中的block執(zhí)行一次之后就被清除出棧了辽幌,所以無法多次使用。
_NSConcreteMallocBlock
類型的block有閉包行為椿访,并且該block需要被多次執(zhí)行乌企。當(dāng)需要多次執(zhí)行時,就會把該block從棧中復(fù)制到堆中成玫,供以多次執(zhí)行加酵。
編譯器如何編譯
我們通過一個簡單的示例來說明:
#import <dispatch/dispatch.h>
typedef void(^BlockA)(void);
__attribute__((noinline))
void runBlockA(BlockA block) {
block();
}
void doBlockA() {
BlockA block = ^{
// Empty block
};
runBlockA(block)
;}
上面的代碼定義了一個名為BlockA
的block類型,該block在函數(shù)doBlockA
中實現(xiàn)哭当,并將其作為函數(shù)runBlockA
的參數(shù)猪腕,最后在函數(shù)doBlockA
中調(diào)用函數(shù)runBloackA
。
注意:如果block的創(chuàng)建和調(diào)用都在一個函數(shù)里面钦勘,那么優(yōu)化器(optimiser)可能會對代碼做優(yōu)化處理陋葡,從而導(dǎo)致我們看不到編譯器中的一些操作,所以用__attribute__((noinline))
給函數(shù)runBlockA
添加noinline
彻采,這樣優(yōu)化器就不會在doBlockA
函數(shù)中對runBlockA
的調(diào)用做內(nèi)聯(lián)優(yōu)化處理腐缤。
我們來看看編譯器做的工作內(nèi)容:
#import <dispatch/dispatch.h>
__attribute__((noinline))
void runBlockA(struct Block_layout *block) {
block->invoke();
}
void block_invoke(struct Block_layout *block) {
// Empty block function
}
void doBlockA() {
struct Block_descriptor descriptor;
descriptor->reserved = 0;
descriptor->size = 20;
descriptor->copy = NULL;
descriptor->dispose = NULL;
struct Block_layout block;
block->isa = _NSConcreteGlobalBlock;
block->flags = 1342177280;
block->reserved = 0;
block->invoke = block_invoke;
block->descriptor = descriptor;
runBlockA(&block);
}
上面的代碼結(jié)合block的數(shù)據(jù)結(jié)構(gòu)定義,我們能很容易得理解編譯器內(nèi)部對block的工作內(nèi)容肛响。
copy()和dispose()
上文中提到岭粤,如果我們想要在以后繼續(xù)使用某個block,就必須要對該block進行拷貝操作特笋,即從椞杲剑空間復(fù)制到堆空間。所以拷貝操作就需要調(diào)用Block_copy()
函數(shù),block的descriptor
中有一個copy()
輔助函數(shù)虎囚,該函數(shù)在Block_copy()
中執(zhí)行臼寄,用于當(dāng)block需要拷貝對象的時候,拷貝輔助函數(shù)會retain
住已經(jīng)拷貝的對象溜宽。
既然有有copy
那么就應(yīng)該有release
吉拳,與Block_copy()
對應(yīng)的函數(shù)是Block_release()
,它的作用不言而喻适揉,就是釋放我們不需要再使用的block留攒,block的descriptor
中有一個dispose()
輔助函數(shù),該函數(shù)在Block_release()
中執(zhí)行嫉嘀,負(fù)責(zé)做和copy()
輔助函數(shù)相反的操作炼邀,例如釋放掉所有在block中拷貝的變量等。
總結(jié)
以上內(nèi)容是我學(xué)習(xí)各大師的文章后對自己學(xué)習(xí)情況的一個記錄剪侮,其中有部分文字和代碼示例是來自大師的文章拭宁,還有一些自己的理解,如有錯誤還請大家勘誤瓣俯。