从内存分配角度分析c和java里的static 关键字.
??? 即使作為Java的初學(xué)者, 對this 和 static 這兩個關(guān)鍵字都不會陌生. 其實也不難理解:
??? this 關(guān)鍵字:? 指的是對象的本身(注意不是類本身)? 跟.net 語言的Me 關(guān)鍵字類似.
??? static 關(guān)鍵字: 靜態(tài)分配的對象或?qū)ο蟪蓡T.? 也就是指被static 修飾的成員只屬于類本身, 而不會想其他成員一樣會對每個對象單獨分配.
??? 但是c語言也有static關(guān)鍵字, 但是c語言中的static并不只是靜態(tài)分配的意思,如果用在靜態(tài)局部變量(函數(shù)內(nèi)部), 則是說明這個變量是靜態(tài)的,?? 如果用在全局變量或函數(shù), 則是防止函數(shù)或全程變量被其他c文件中的函數(shù)訪問(通過include 頭文件).? 為什么Java里的static 會跟c 語言里的有這種區(qū)別呢.
??? 下面會從內(nèi)存分配的角度淺析一下這個問題.
一. C語言程序所占內(nèi)存的大概結(jié)構(gòu)
?? ? ? 我們知道, static 的意思是靜態(tài)分配, 那么到底什么是靜態(tài)分配和動態(tài)分配呢.? 其實內(nèi)存的靜態(tài)分配和動態(tài)分配是對于C/C++ 來講的.? 而Java 作為由C/C++ 發(fā)展而來的類C語言, 雖然把內(nèi)存管理這一塊砍掉了(對程序員屏蔽, 在Java底層處理), 但是還是繼承了C語言的一些特性.
??
?????? 所以Java里有些特性和概念, 通過C語言分析能更好的理解.
?????? 首先, 1個由C語言編譯的程序所使用的內(nèi)存大概分成如下幾個部分
?????? 1、棧區(qū)(stack)— 由編譯器自動分配釋放 ,存放函數(shù)的參數(shù)值,局部變量的值等。其操作方式類似于數(shù)據(jù)結(jié)構(gòu)中的棧
?????? 2、堆區(qū)(heap) — 一般由程序員分配釋放, 若程序員不釋放,程序結(jié)束時可能由OS回收 。
?????? 3、全局區(qū)(靜態(tài)區(qū))(static) --- 用來存放全局變量和靜態(tài)局部變量.
?????? 4、文字常量區(qū) —常量字符串就是放在這里的。 程序結(jié)束后由系統(tǒng)釋放 ?
?????? 5、程序代碼區(qū)—存放函數(shù)體的二進制代碼。
?????? 下面是大致的圖解.
二. C語言內(nèi)存的靜態(tài)分配和動態(tài)分配.
2.1 靜態(tài)分配內(nèi)存
????? 對于C語言來講, 靜態(tài)分配內(nèi)存就是只用 類型 + 變量名 定義的變量. 不管這個變量是局部變量(函數(shù)中) 或全局變量(函數(shù)外).
2.1.1 局部變量 ?
???? 第一種情況,? 函數(shù)中靜態(tài)分配內(nèi)存的局部變量.
???
int f(){ int j=20; return j; }?????
???? 如上面那個簡單的例子,? 在f()函數(shù)里定義的局部變量j 就是1個 靜態(tài)分配內(nèi)存的變量(注意不是靜態(tài)變量).???? 這種靜態(tài)分配的變量的生存周期就是函數(shù)執(zhí)行一次的周期.
???? 什么意思呢,? 就是當(dāng)f()執(zhí)行時, 操作系統(tǒng)會為變量j 分配1個字節(jié)(32位系統(tǒng))的內(nèi)存,? 但是當(dāng)f() 執(zhí)行完時.? 變量j所占的內(nèi)存就會被釋放. 可以操作系統(tǒng)用作它用.
????? 也就是說, 當(dāng)f() 被循環(huán)執(zhí)行1萬次, 程序并不會額外占用9MB多的內(nèi)存,? 因為j所占的內(nèi)存會不斷地釋放分配.
????? 上面提過了,? 局部變量所占的內(nèi)存是分配在內(nèi)存里的Stuck(棧)區(qū)的. 因為j是int 類型, 所以它會在stuck區(qū)占用1字節(jié)
?????
注: 局部變量還有另1種形式就是函數(shù)的參數(shù):
如下面的變量i也是局部變量:
int f(int i){i++;return i; }1.1.2 全局變量
???? 第二種情況就是定義在函數(shù)外(c文件中)靜態(tài)分配的變量.??? 例子:
int j ;int f(){ j+=20; return j; }???? 上面的j就是全局變量了.
???? 全局變量可以被各個函數(shù)調(diào)用, 所以全局變量j的生存周期跟函數(shù)f()無關(guān).
???? 也就是說, 全局變量的生存周期是就是程序的生存周期.
????? 即是,如果程序一直在運行,? 變量j所占的內(nèi)存就不會被釋放.? 理論上講, 定義的全局變量越多, 程序所占的內(nèi)存就越大.
???? 可見全局變量在內(nèi)存中是靜態(tài)的, 一旦被分配內(nèi)存.它不會被釋放和重新分配. 所以它占用的內(nèi)存被分配在內(nèi)存里的靜態(tài)區(qū):
2.2 動態(tài)分配內(nèi)存
????? c語言所謂動態(tài)分配內(nèi)存就是使用malloc 函數(shù)(必須引用stdlib.h) 在內(nèi)存中劃分一個連續(xù)區(qū)間(heap區(qū)), 然后用1個 指針變量接受把這區(qū)間的 頭部地址.? 這個指針變量所指向的內(nèi)存就是動態(tài)分配的內(nèi)存.
????? 舉個例子:
#include <stdio.h> #include <stdlib.h>int * f(){//error, variable with static memory allocation cannot be past to another function.//int j = 20; int * p = &j; int * p = (int *)malloc(sizeof(int)); //goodreturn p; }int main(){int * q = f();*q = 20;printf ("*q is %d\n", *q);free(q);q=NULL; }參考上面那個小程序.
在函數(shù)f()中.
這條語句首先定義了1個int類型的指針 p. 這個指針本身是靜態(tài)分配的.
在heap去劃分了1個size為1個int(1字節(jié))的動態(tài)內(nèi)存,? 并且將該動態(tài)內(nèi)存的頭部地址賦給指針p.
最終這個函數(shù)f()返回了指針p的值, 也就是動態(tài)內(nèi)存的頭部地址
在main()函數(shù)中,
再定義1個靜態(tài)分配的指針q, 用來接受函數(shù)f()返回的動態(tài)內(nèi)存的頭部地址.
一旦f()函數(shù)執(zhí)行完,? f()里的指針p本身會被釋放,? 但是p指向的動態(tài)內(nèi)存仍然存在, 而且它的頭部地址被賦給了main()函數(shù)的指針q.
然后, q使用了這個動態(tài)內(nèi)存.(賦值20)
動態(tài)內(nèi)存的生命周期也是整個程序, 但是動態(tài)內(nèi)存可以被手動釋放. 在main()函數(shù)的最后使用了free()函數(shù)釋放了指針q,也就是q指向的動態(tài)內(nèi)存.
如果不手動釋放, 那么當(dāng)f() 和 main()函數(shù)被循環(huán)多次執(zhí)行時, 就會多次在heap區(qū)劃分內(nèi)存, 造成可用內(nèi)存越來越少,就是所謂的內(nèi)存泄露了.
圖解:
1. main()函數(shù)定義指針q, 這個指針q本身是1個局部變量, 在棧區(qū)分配內(nèi)存.
2. main()函數(shù)調(diào)用f()函數(shù), 局部變量指針q在f()里定義, 在棧區(qū)分配內(nèi)存
3. 在heap區(qū)劃分一塊動態(tài)內(nèi)存(malloc函數(shù))
4? 把動態(tài)內(nèi)存的頭部地址賦給f()函數(shù)里的p
5. f()函數(shù)執(zhí)行完成, p的值(就是動態(tài)內(nèi)頭部地址)賦給了main()函數(shù)的指針q, 這時q指向了動態(tài)內(nèi)存. p本身被釋放.
6. 當(dāng)動態(tài)內(nèi)存被使用完后, 在mian()函數(shù)的最后利用free()函數(shù)把動態(tài)內(nèi)存釋放, 這個動作相當(dāng)重要.
7. 當(dāng)mian()執(zhí)行完時, 指針p本身也會被釋放.
2.3 動態(tài)分配內(nèi)存和靜態(tài)分配內(nèi)存的區(qū)別.
由上面的例子可以看出, 動態(tài)內(nèi)存與靜態(tài)內(nèi)存有如下的區(qū)別.
2.靜態(tài)變量在內(nèi)存里的棧區(qū)(局部變量)或全局區(qū)(全局變量 or 靜態(tài)局部變量(后面會提到))里分配.? 動態(tài)分配內(nèi)存的變量在heap區(qū)分配.
3.靜態(tài)分配內(nèi)存變量生命周期有兩種, 其中局部變量,在函數(shù)結(jié)束后就會被釋放, 而全局變量在程序結(jié)束后才釋放.? 而動態(tài)變量需要程序員手動釋放, 否則會在程序結(jié)束后才釋放.
2.4 動態(tài)分配內(nèi)存的優(yōu)缺點
2.4.1 動態(tài)分配內(nèi)存的三個優(yōu)點
看起來動態(tài)分配內(nèi)存的變量的使用貌似比靜態(tài)分配內(nèi)存的變量使用麻煩啊.? 單單1個malloc函數(shù)都令新人覺得頭痛.
但是動態(tài)分配的內(nèi)存有三個優(yōu)點.
1.可以跨函數(shù)使用. 參見上面的例子, main()函數(shù)使用了f()函數(shù)定義的動態(tài)內(nèi)存.
?? 有人說全局變量也可跨函數(shù)使用啊, 的確. 但是全局變量必須預(yù)先定義(預(yù)先占用內(nèi)存), 而動態(tài)分配內(nèi)存可以再需要時分配內(nèi)存. 更加靈活.
??? 關(guān)于跨函數(shù)使用內(nèi)存可以參考我另1篇博文:
http://blog.csdn.net/nvd11/article/details/8749395
2.? 可以靈活地指定或分內(nèi)存的大小.
??? 例如 (int *)malloc(sizeof(int) * 4) 就劃分了4個字節(jié)的內(nèi)存(動態(tài)數(shù)組).?? 這個特性在定義動態(tài)數(shù)組時特別明顯.
??? 而且可以用realloc 函數(shù)隨時擴充或減少動態(tài)內(nèi)存的長度.?
???? 這個特性是靜態(tài)分配內(nèi)存的變量不具備的.
3. 動態(tài)變量可以按需求別手動釋放.
??? 雖然局部變量隨函數(shù)結(jié)束會自動釋放,? 而動態(tài)分配的內(nèi)存甚至能在函數(shù)結(jié)束前手動釋放.
??? 而全局變量是不能釋放的. 所以使用動態(tài)內(nèi)存比使用全局變量更加節(jié)省內(nèi)存.
2.4.2 動態(tài)分配內(nèi)存的兩個硬傷
但是動態(tài)分配內(nèi)存也有2個硬傷:
1.? 就是必須手動釋放....? 否則會造成內(nèi)存泄露.
其實上面都講過了, 這里舉個具體例子:
程序1:
#include <stdio.h> #include <stdlib.h>int f(int i){int * p = (int *)malloc(sizeof(int));*p = i*2;printf ("*p is %d\n", *p);free(p);p=NULL;return 0; }int main(){int i;for (i=0; i<100; i++){f(i); } }程序1的f() 函數(shù)被main()函數(shù)循環(huán)執(zhí)行了100次,?? 所以f()在內(nèi)存heap區(qū)劃分了100次動態(tài)內(nèi)存,? 但是每一次f()結(jié)束前都會用free函數(shù)將其手動釋放.
所以并不會造成內(nèi)存浪費.? 這里再提一提, free(p) 這個函數(shù)作用是釋放p所指向的動態(tài)內(nèi)存,? 但是指針p的值不變, 仍然是那個動態(tài)內(nèi)存的地址.? 如果下次再次使用p就肯定出錯了.所以保險起見加上p=NULL, 以后使用p之前,也可以用NULL值判斷它是否被釋放過.
程序2:
#include <stdio.h> #include <stdlib.h>int f(int i){int * p = (int *)malloc(sizeof(int));*p = i*2;printf ("*p is %d\n", *p);return 0; }int main(){int i;for (i=0; i<100; i++){f(i); } }上面就是反面教材. 如果沒有手動釋放, 當(dāng)f()函數(shù)被循環(huán)執(zhí)行時就會多次劃分動態(tài)內(nèi)存, 導(dǎo)致可用內(nèi)存越來越少. 也就是程序所占的內(nèi)存越來越多, 這就是傳說中的內(nèi)存泄露.
可能有人認(rèn)為, 不就是加多1個free()函數(shù)嗎?? 算不上缺點.
但是有時候程序猿很難判斷一個指針該不該被free() 函數(shù)釋放..
程序3:
#include <stdio.h> #include <stdlib.h>int main(){int * p = (int *)malloc(sizeof(int));int * q = p;*p = 10;printf ("*p is %d\n", *p);*q = 20;printf ("*q is %d\n", *q);free(p);free(q); //error, the dynamic memory is released already.return 0; }看看上面的例子,
指針p和q指向同1個動態(tài)內(nèi)存.
當(dāng)執(zhí)行free(p)時,? 釋放的是動態(tài)內(nèi)存, 而不是釋放p本身,? 所以再此執(zhí)行free(q)就出錯了,?? 因為那個動態(tài)內(nèi)存已經(jīng)被釋放過了嘛..
有人覺得, 這個錯誤也不難發(fā)現(xiàn)嘛.., 小心點就ok了.
首先, 上面的代碼編譯時并不會報錯, 至少gcc會編譯通過, 執(zhí)行時才會出錯...
而且, 當(dāng)項目越來越復(fù)雜時, 可能有多個指針指向同1個動態(tài)內(nèi)存,? 而且某個指針指向的動態(tài)內(nèi)存是其他程序員在其他函數(shù)內(nèi)分配的..
這時你就很難判斷了,? 如果不釋放怕造成內(nèi)存泄露,? 如果釋放了, 別的程序猿不知道的話再次使用...就會出錯.
所以有時候在項目中程序猿很難判斷1個指針該不該釋放啊... 特別是多個程序猿合作的大型c項目.
這就是為什么說c語言功能強大, 但是不適合編寫大型項目的原因之一,? 需要程序猿有相當(dāng)扎實的內(nèi)存管理能力.
2.? 另個硬傷就是內(nèi)存溢出.
什么是內(nèi)存溢出呢,? 就是使用了動態(tài)分配內(nèi)存長度之外的內(nèi)存...
舉個例子:
#include <stdio.h> #include <stdlib.h>int main(){int * p = (int *)malloc(sizeof(int) * 4);*p = 1;*(p + 1) = 2;*(p + 2) = 3; *(p + 3) = 4;*(p + 4) = 5; // error,memory overflowint i;for (i=0;i < 5;i++){ // error, should be i < 4printf("no.%d is %d\n",i,*(p+i));}free(p);p=NULL;return 0; }上面定義了長度為4的連續(xù)內(nèi)存空間. (動態(tài)整形數(shù)組)
但是這個程序卻使用了長度為5的內(nèi)存空間, 也就是這個動態(tài)內(nèi)存后面額外的的那一個字節(jié)的內(nèi)存被使用了.
其實就是p+4 這個地址的內(nèi)存并沒有定義, 但是卻被使用, 這就是傳說中的內(nèi)存溢出.
這個代碼可以被正常編譯, 可怕的是, 很多情況下它會正常執(zhí)行...
但是如果p+4剛好被這個程序的其他變量或其他程序正在使用, 而你卻往它寫入數(shù)據(jù), 則可能會發(fā)生導(dǎo)致程序漰潰的錯誤...
這就是有些c \ c++ 程序不夠健壯的原因, 有時候會發(fā)生崩潰..
所以說c語言很難就難在這里, 內(nèi)存管理啊.
三. C語言的static關(guān)鍵字
終于講到正題了, 下面就說說c語言static關(guān)鍵字對內(nèi)存分配的影響.
首先, c的static 關(guān)鍵字是不能修飾動態(tài)分配內(nèi)存的.
例如
int * p = static (int *)malloc(sizeof(int))是錯誤的.??
?但是 下面寫法是合法的.
static int * p = (int *)malloc(sizeof(int))上面的static 不是修飾動態(tài)分配的內(nèi)存,? 而是修飾靜態(tài)分配的指針變量p
上面也提到過了, c語言的static 可以修飾如下三種對象:
1. 全局變量和函數(shù)
2. 函數(shù)內(nèi)的局部變量.
注意, c語言結(jié)構(gòu)體的成員不能用static 修飾
3.1 static 修飾全局變量或函數(shù).
在全局變量和函數(shù)名前面的 static函數(shù)并不影響 對象的內(nèi)存分配方式,
static 修飾的全局變量還是被分配與全局區(qū)中.???
而被static修飾的函數(shù)的2進制代碼還是被分配于程序代碼區(qū)中.
這種情況下 static 的作用只是簡單地對其他c文件的函數(shù)屏蔽.
也就是1個c文件a.c,? 引用了另一個c文件b.c
那么a.c 文件就不能訪問b.c 文件里用static修飾的 全局變量和函數(shù).
3.2 static 修飾局部變量.
如果用static 來修飾c語言函數(shù)中的局部變量, 那么這個局部變量是靜態(tài)局部變量了.
如下面的例子:
#include <stdio.h> #include <stdlib.h>int f(){int i = 1; // local variablestatic int j = 1; // static local variablei++; j++;printf("i is %d, j is %d\n",i,j);return 0; }int main(){int i;for (i=0;i < 10 ;i++){f();}return 0; }上面的f() 函數(shù)i就是 一般的局部變量了, 而 j 前面有static修飾, 所以j是1個靜態(tài)局部變量.
如果上面的f()函數(shù)被連續(xù)執(zhí)行10次, 那么 i 和 j的值是不同的.
gateman@TFPC tmp $ ./a.out i is 2, j is 2 i is 2, j is 3 i is 2, j is 4 i is 2, j is 5 i is 2, j is 6 i is 2, j is 7 i is 2, j is 8 i is 2, j is 9 i is 2, j is 10 i is 2, j is 11可以見到, 每次f()執(zhí)行,? i 的值都是2,? 而 j 的 值 會不斷加1.
原因就是static 用在局部變量前面就會改變該局部變量的內(nèi)存分配方式.
上面說過, 一般局部變量是放在內(nèi)存Stuck區(qū)的,? 而靜態(tài)局部變量是放在全局(靜態(tài))區(qū)的.
圖解:
當(dāng)程序執(zhí)行時,? f()作為1個函數(shù),? 它的2進制代碼是存放在內(nèi)存里的程序代碼區(qū)的.
對于變量i:
??????? f()每次執(zhí)行時都會在Stuck區(qū)為變量i初始化一塊內(nèi)存.? 而結(jié)束時會自動地把該內(nèi)存釋放,
??????? 也就是說int i = 1; 這個語句每次執(zhí)行時都會執(zhí)行.? 所以i每次輸出的值都是一樣的.
對于靜態(tài)局部變量j:
??????? f() 會在內(nèi)存Static區(qū)檢測有無屬于變量j的內(nèi)存.??????? 如果無, 則執(zhí)行初始化語句 static int j = 1;? 并記錄下該內(nèi)存的地址.
??????? 如果有, 則直接使用該內(nèi)存.
??????? 當(dāng)f() 執(zhí)行完成時, 該內(nèi)存不會被釋放.
??????? 也就是講, 當(dāng)f()下一次執(zhí)行時, 就不會執(zhí)行 static int j =1; 這條語句.
??????? 所以當(dāng)f()循環(huán)執(zhí)行時, j的值就會遞增了.
也就是講, 當(dāng)static 修飾1個局部變量時, 會更改局部變量的內(nèi)存分配方式.而且這個局部變量的生命周期就會變成全局變量一樣.
全局變量與靜態(tài)局部變量的區(qū)別:
由此可見, 靜態(tài)局部變量與全局變量的內(nèi)存分配方式是類似的, 它們的內(nèi)存都是被分配在static 區(qū), 那么它們的生命周期也是一樣的,都是整個程序的生命周期. 而它們的區(qū)別如下: 1. 全局變量在程序開始時就分配內(nèi)存, ?而靜態(tài)局部變量在對應(yīng)函數(shù)第一次執(zhí)行時分配內(nèi)存.2. 全局變量能被各個函數(shù)訪問, 所以一般用于傳遞數(shù)據(jù). ?靜態(tài)局部變量只能被定義它的函數(shù)訪問, ?一般用于保存特定數(shù)據(jù). ? ?
四. Java語言程序所占內(nèi)存的大概結(jié)構(gòu)
Java 作為C/C++ 發(fā)展出來的語言, 最大的區(qū)別就是對程序員管理屏蔽了內(nèi)存管理的部分. ? 也就是說Java沒有了指針這個概念. 所有動態(tài)內(nèi)存的分配和釋放都在Java底層里自動完成.
所以說Java 的功能和性能都遠比不上C/C++ .
但是正因為從根本上避免了內(nèi)存泄漏等內(nèi)存操作容易產(chǎn)生的錯誤, 所以Java編寫的程序的健壯性會很好, 也就是Java比C語言更適合大型項目的原因.
Java畢竟也是類C語言的一種, 所以Java的內(nèi)存結(jié)構(gòu)跟C語言類似:
如圖:
可見java的程序會把其占用的內(nèi)存大概分成4個部分.
Stuck 區(qū): 跟c一樣, 存放局部變量, 也就是函數(shù)內(nèi)定義的變量.
Heap 區(qū): 跟c一樣, 存放動態(tài)分配內(nèi)存的變量, 只不過動態(tài)分配內(nèi)存的方式跟c不通, 下面會重點提到.
數(shù)據(jù)區(qū): ? ?相當(dāng)于c的static區(qū), 存放靜態(tài)(static)變量和字符串常量
代碼區(qū): ? ?跟c一樣, 存放2進制代碼.
五. Java內(nèi)存的靜態(tài)分配和動態(tài)分配.
跟C一樣, Java的變量的內(nèi)存分配也可以分成靜態(tài)分配內(nèi)存和動態(tài)分配兩種, 只不過形式上跟c語言可以講存在很大的差別.
5.1 Java靜態(tài)分配內(nèi)存
上面提到了, c語言靜態(tài)分配的內(nèi)存(非static 修飾)可以分成兩種: ?全局變量和局部變量. ?
其中全局變量在函數(shù)外定義, 內(nèi)存分配在static區(qū). ? ?而局部變量在函數(shù)內(nèi)定義, 內(nèi)存分配在stuck區(qū).
而Java 里是不存在全局變量這玩意的. ?因為Java是1個完全面向?qū)ο蟮恼Z言, ?一旦1個變量不是在函數(shù)里定義, 那么他就是在類里面定義, 就是1個類的成員了.
如下面這個例子:
public class A{int j;int f(){int i = 0;return i;} }其中, 變量j是A的1個成員, 而不是全局變量.
而變量i 跟c一樣, 是屬于函數(shù)f的1個局部變量.
java里局部變量的內(nèi)存方式跟c語言是一樣的, 都是屬于靜態(tài)分配, ?內(nèi)存被分配在stuck區(qū).
那么其生命周期就也會隨函數(shù)執(zhí)行完成而結(jié)束, 這里就不細講了.
5.2 Java動態(tài)分配內(nèi)存
我們看回上面那個例子.Class A里面的變量j 其實就是A的一個成員, ?而Java里1個類里面的所有非Static修飾的成員都是動態(tài)分配內(nèi)存的. 也就是講, 其實那個變量j是動態(tài)分配內(nèi)存的, 內(nèi)存被分配在heap區(qū)里.
怎么講呢, ?還是要借助c語言:..
2.2.1 c語言靜態(tài)分配內(nèi)存的結(jié)構(gòu)體
面向?qū)ο蟾旧鲜?種編程的思想, 其實作為面向過程的c語言, 也可以用面向?qū)ο蟮乃枷雭砭幊?.大家都知道c語言里具有類的雛形---> 結(jié)構(gòu)體. 其本質(zhì)就是讓不同的數(shù)據(jù)類型集合存儲.
首先看看結(jié)構(gòu)體的靜態(tài)分配內(nèi)存用法:
#include <stdio.h> #include <stdlib.h> #include <string.h>struct A{int id;char name[16]; };int main(){struct A a;a.id = 1;strcpy(a.name, "Jack");printf("%d, %s\n", a.id, a.name);return 0; }
上面的簡單例子, 定義并使用了1個結(jié)構(gòu)體A.
下面1句1句從內(nèi)存分配的角度詳細講解...
首先:
struct A{int id;char name[16]; }
這幾句代碼定義了結(jié)構(gòu)體A的結(jié)構(gòu), 包括1個整形和1個字符數(shù)組的兩個成員.
在程序執(zhí)行過程中, ?定義代碼會作為2進制代碼存放于內(nèi)存里的代碼區(qū)中.
struct A a;
這代碼靜態(tài)定義了1個結(jié)構(gòu)體a. ?它的長度是4 + 16 =20 byte 而內(nèi)存是被分配在 stuck區(qū)的.
注意, 這個時候, A里面的兩個成員: ?id 和 name里面是垃圾值, 并沒有初始賦值的.
a.id = 1; strcpy(a.name, "Jack");
這兩個就是為結(jié)構(gòu)體a的兩個成員賦值了. 不多說..
圖解:
2.2.2 c語言靜態(tài)分配內(nèi)存的結(jié)構(gòu)體的缺點
這種靜態(tài)定義使用的結(jié)構(gòu)體優(yōu)點很簡答: 方便使用, 安全性好.
缺點是什么呢?
當(dāng)然了, 上面都提過:
1. 不能夸函數(shù)使用, 生命周期隨函數(shù)結(jié)束.
2. 不能靈活釋放.
其實這兩個缺點都系虛的.
真正的問題是, 在生產(chǎn)中, ?1個結(jié)構(gòu)體往往定義得十分復(fù)雜. ?也就是包含幾十個成員, 幾十個函數(shù)指針(方法).
那么這個結(jié)構(gòu)體所占的內(nèi)存就很客觀了,
棧的內(nèi)存大小有限, ?而在heap區(qū)能申請更大的內(nèi)存, ?這個才是動態(tài)分配內(nèi)存的結(jié)構(gòu)體的必要性.
2.2.3 c語言動態(tài)分配內(nèi)存的結(jié)構(gòu)體
看下面的例子: #include <stdio.h> #include <stdlib.h> #include <string.h>struct A{int id;char name[16];void (* A_prinf)(struct A *); };void printf_A(struct A * b){printf("%d, %s\n", b->id, b->name); }struct A * A_new(int id, char * name){struct A * b = (struct A *)malloc(sizeof(struct A));b->A_prinf = printf_A;b->id = id;strcpy(b->name,name);return b; }int main(){struct A * a;a = A_new(1,"Jack");a->A_prinf(a);free(a);a = NULL;return 0; }上面就是動態(tài)分配內(nèi)存的結(jié)構(gòu)體最常用的用法..
再一句一句來: struct A{int id;char name[16];void (* A_prinf)(struct A *); };
這段定義了1個結(jié)構(gòu)體A, 跟之前例子不同的是只不過, 多了1個函數(shù)指針, 用于打印這個結(jié)構(gòu)體的成員. 這段2進制代碼一樣存放子在代碼區(qū)中.
void printf_A(struct A * b){printf("%d, %s\n", b->id, b->name); }
上面是打印函數(shù)的定義了, ?我們會將結(jié)構(gòu)體A的指針指向這個函數(shù). 也會放在代碼區(qū)中.
struct A * A_new(int id, char * name){struct A * b = (struct A *)malloc(sizeof(struct A));b->A_prinf = printf_A;b->id = id;strcpy(b->name,name);return b; }
A_new()函數(shù)相當(dāng)于1個初始化函數(shù).
無論靜態(tài)或動態(tài)定義1個結(jié)構(gòu)體之后, 只會在stuck區(qū)或heap區(qū)分配該內(nèi)存. 而內(nèi)存里結(jié)構(gòu)體的成員是垃圾數(shù)據(jù)().
也就是說,定義1個結(jié)構(gòu)體A的"對象"b后, ?b的id, name, 函數(shù)指針A_prinft都是垃圾數(shù)據(jù).
如果不經(jīng)初始化, 直接使用成員, 例如函數(shù)指針的話, 系統(tǒng)就出錯了.
所以初始化函數(shù)最重要的作用就是把每1個?對象的函數(shù)指針指針向正確的函數(shù). ?這個就是初始化函數(shù)的必要性.(當(dāng)然你也可以在main函數(shù)內(nèi)手動指向).
這里順便加兩個參數(shù), 把id和name也初始化了.
struct A * a;
注意, main函數(shù)里這1句定義的是1個結(jié)構(gòu)體A指針, 而不是結(jié)構(gòu)體.
任何類型的指針長度都是4byte(32 位系統(tǒng)), ?而這個指針是局部變量, ?會被分配在stuck區(qū)
那么在棧區(qū)的指針a就指向堆區(qū)的內(nèi)存了.
所以, 實際上是調(diào)用了printf_A(). ?但是,參數(shù)是必要的.
后面的就是釋放內(nèi)存和致空指針. 因為c語言不會自動釋放動態(tài)內(nèi)存.
圖解:
其實單單只看這個例子main()函數(shù)的代碼, 是不是覺得很像面向?qū)ο笳Z言java 或 C++.
所以講, 面向?qū)ο笃鋵嵤?種編程思想. ?而Java的內(nèi)部實現(xiàn)還是離不開c/c++.
我們也可以在這里看出面向?qū)ο蟮囊恍┨匦? 這實際上也是動態(tài)分配內(nèi)存的優(yōu)點:
1. 對象的指針存放在棧區(qū), 而無論1個結(jié)構(gòu)體的內(nèi)存占用有多么龐大, 棧區(qū)的對象指針只會保存結(jié)構(gòu)體內(nèi)存的頭部指針.?
? ? 所以棧區(qū)的單個對象指針只占用4byte. ?相對于靜態(tài)分配的結(jié)構(gòu)體, 大大節(jié)省了棧區(qū)的空間.
2. 多個不同的對象會利用不同的heap區(qū)內(nèi)存存放各種的成員(例如id, name), ?但是各自的函數(shù)指針指向相同的代碼區(qū)函數(shù).
? ?也就是說, 每1個結(jié)構(gòu)體對象的內(nèi)部函數(shù)實際上都是一樣的 (除非手動再指向). 只不過參數(shù)不同.
2.2.3 Java里的類動態(tài)分配.
終于講回java了, 上面提到那么多c語言的東西其實是為了與java作對比, 更好地了解java里類的內(nèi)存分配.Java 是完全面向?qū)ο笳Z言, 所以任何東西都必須用類來描述. ?而Java的類實際上是由c語言的結(jié)構(gòu)體擴展而來的. ? 而上面說過, 生產(chǎn)環(huán)境中的類往往非常復(fù)雜. 所以 Java 里的類都是動態(tài)分配內(nèi)存的.
看下面這個簡單例子: class B{int i = 10;int f(){System.out.printf("i is %d\n", i);return 0;} }public class A{public static void main(String[] args){B b;b = new B();b.i = 11;b.f();} }
上面的例子定義了兩個類,
在A的入口函數(shù)中, ?定義并實例化了類B. 下面講下, 類B是如何被分配內(nèi)存的.
class B{int i = 10;int f(){System.out.printf("i is %d\n", i);return 0;} }
上面就是類B的定義代碼, ?跟c語言的結(jié)構(gòu)體定義代碼一樣, 它也會被轉(zhuǎn)成2進制代碼而存放在代碼區(qū)中.
跟c語言的結(jié)構(gòu)體代碼做下對比: struct A{int id;char name[16];void (* A_prinf)(struct A *); };
實際上類與結(jié)構(gòu)體成員的定義都是類似的.
但是 結(jié)構(gòu)體只是一個簡單的不同類型的集合體. 里面的成員(包括函數(shù)指針)不允許具有初始值.
而類是允許的. 而且函數(shù)的定義代碼也寫在類里面..
B b;b = new B();b.i = 11;b.f();
我們看看這四句在入口函數(shù)的代碼. B b;
?這個語句在c語言中可以理解成 靜態(tài)定義1個B的結(jié)構(gòu)體對象b.
但是在Java中, 我們應(yīng)該理解成為定義1個類B的指針b, ?注意java里雖然取消了指針操作, 但是java里的底層很多東西都還是需要指針來實現(xiàn).
相當(dāng)于c 語言里的
B * b;所以這一句理解為簡單地定義1個局部指針變量b, 它的內(nèi)存是分配在stuck區(qū)的.
既然對象b只是相當(dāng)于一個指針. 當(dāng)執(zhí)行完這一句時, 它只是1個空指針, 所以它指向的內(nèi)存并不能使用.?
而我們就說對象b并沒有實例化.
當(dāng)我們直接對對象b的非static成員操作時就會彈出錯誤: 對象沒有實例化了, ?就是這個原因.
接下來這一句就比較重要了.
首先 new B() ?這個作用就是在heap區(qū)動態(tài)分配1個類B的內(nèi)存,其實就是相當(dāng)于c語言里的 (B *)malloc(sizeof(B))啦.
只不過在Java里, ?java把 malloc分配內(nèi)存的動作隱藏在new這個語句里面. ??
跟c語言一樣, 分配內(nèi)存后還需要把內(nèi)存的頭部指針賦于對象b. ?所以會有"b =" 這個寫法.
其實就相當(dāng)于c語言里的
b = (B *)malloc(sizeof(B))當(dāng)執(zhí)行完這一句后
對象b實際上就指向了heap區(qū)的對應(yīng)內(nèi)存.
這時我們就可以對對象b的成員進行操作. 也就是對象b已經(jīng)被實例化.
所以其實實例化的真正意思就是1個對象指向了heap區(qū)的對應(yīng)內(nèi)存.
如圖:
這時, 我們看看對象b的成員i, ?它隨著對象b的內(nèi)存, 同樣被分配在heap區(qū)里面.
所以我們就說 在函數(shù)外定義的非static變量不是全局變量, ?而是類的成員, 它們是動態(tài)分配的.
既然new出來的東西是動態(tài)分配, 那么就需要手動釋放? ?java里有自動釋放的機制, 所以不必程序猿手動釋放了.
六. Java里的static關(guān)鍵字
但是有一種情況, ?有些類的成員并不需要實例化就可以使用, ?那么這種成員就是靜態(tài)成員, 肯定是用static修飾的. class B{int i = 1;int f(){System.out.printf("i is %d\n", i);return 0;}static int j = 1;static int g(){System.out.printf("j is %d\n", j);return 0;} }public class A{public static void main(String[] args){B.j += 1;B.g();B b;b = new B();b.i = 10;b.j += 1;b.f();b.g();} }看看上面這個經(jīng)過簡單修改過的例子. B增加1個靜態(tài)成員j, 1個靜態(tài)函數(shù)g();
接下來從這個例下分析下java靜態(tài)成員的一些特性.
6.1 static 成員不需實例化就可以使用.
上面入口函數(shù)中直接使用了類B的靜態(tài)成員j 和 靜態(tài)函數(shù)g() B.j += 1; B.g();這種情況其實就是把 成員j的內(nèi)存分配在了java的數(shù)據(jù)區(qū)中, 而不是heap區(qū), 這個內(nèi)存地址在程序執(zhí)行時是不變的. 而且在代碼區(qū)的類定義代碼里保留了這個地址(指針)
6.2 多個不同的對象共享1個static 成員.
如上面的例子, 當(dāng)執(zhí)行B.j += 1后, ?j的值變成2. ?然后再實例1個對象b, 執(zhí)行b.j += 1, 那么輸出對象b的成員的值j就是3了.其實因為對象b里面的靜態(tài)成員j指針還是指向static 區(qū)的同1個內(nèi)存地址.
也就是說類B的多個不同對象實際上共享同1個靜態(tài)成員.
那么java里實例化1個具有靜態(tài)成員的對象時, 會同時把靜態(tài)成員的地址放入棧區(qū).
6.3 靜態(tài)函數(shù)不允許使用非靜態(tài)成員.
這個也不難理解, 看上圖, 靜態(tài)函數(shù)g()是可以在實例化之前使用的, 但是如果g() 引用了非靜態(tài)成員i. 那么當(dāng)沒有實例化對象b調(diào)用g()時, 就會找不到成員i的地址.因為成員i必須在實例化之后才會被分配內(nèi)存啊.
這個就是java不允許靜態(tài)函數(shù)調(diào)用非靜態(tài)成員的原因!
6.4 靜態(tài)函數(shù)和非靜態(tài)函數(shù)的區(qū)別.
的確, 靜態(tài)函數(shù)與非靜態(tài)函數(shù)的2進制代碼都是存放在代碼區(qū)里面..class B{int i = 10;int f(){System.out.printf("i is %d\n", i);return 0;} }
看看上面里的例子. 非靜態(tài)函數(shù)f() 是不帶參數(shù)的.. 而且里面的成員i 也不帶參數(shù).
再看看c語言的結(jié)構(gòu)體定義: struct A{int id;char name[16];void (* A_prinf)(struct A *); };
里面的函數(shù)指針必須帶參數(shù).
其實在java里面, 非靜態(tài)函數(shù)已經(jīng)隱藏了1個自帶參數(shù)"this". 因為上面說過了, 所有不同的實例化后的對象里面的成員內(nèi)存都是不同的, 但是它們的函數(shù)都是指向代碼區(qū)同1個函數(shù).
那么調(diào)用這個函數(shù)時, 這個函數(shù)必須知道到底是哪個對象調(diào)用它, 在這里就是到底要輸出哪個對象的成員i.
所以,? 在底層里, 我任務(wù)非靜態(tài)函數(shù)自帶參數(shù)"this"這個隱藏指針.
但是靜態(tài)函數(shù)不同, 因為它不允許使用非靜態(tài)成員. 就無需"this"了.
完...
?
總結(jié)
以上是生活随笔為你收集整理的从内存分配角度分析c和java里的static 关键字.的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在linux命令行 下学习编写java
- 下一篇: Java 多态的简单介绍.