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