即使作为Java的初学者, 对this 和 static 这两个关键字都不会陌生. 其实也不难理解:
this 关键字: 指的是对象的本身(注意不是类本身) 跟.net 语言的Me 关键字类似.
static 关键字: 静态分配的对象或对象成员. 也就是指被static 修饰的成员只属于类本身, 而不会想其他成员一样会对每个对象单独分配.
但是c语言也有static关键字, 但是c语言中的static并不只是静态分配的意思,如果用在静态局部变量(函数内部), 则是说明这个变量是静态的, 如果用在全局变量或函数, 则是防止函数或全程变量被其他c文件中的函数访问(通过include 头文件). 为什么Java里的static 会跟c 语言里的有这种区别呢.
下面会从内存分配的角度浅析一下这个问题.
我们知道, static 的意思是静态分配, 那么到底什么是静态分配和动态分配呢. 其实内存的静态分配和动态分配是对于C/C++ 来讲的. 而Java 作为由C/C++ 发展而来的类C语言, 虽然把内存管理这一块砍掉了(对程序员屏蔽, 在Java底层处理), 但是还是继承了C语言的一些特性.
所以Java里有些特性和概念, 通过C语言分析能更好的理解.
首先, 1个由C语言编译的程序所使用的内存大概分成如下几个部分
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈 2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。 3、全局区(静态区)(static) — 用来存放全局变量和静态局部变量. 4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放 5、程序代码区—存放函数体的二进制代码。
下面是大致的图解.
对于C语言来讲, 静态分配内存就是只用 类型 + 变量名 定义的变量. 不管这个变量是局部变量(函数中) 或全局变量(函数外).
第一种情况, 函数中静态分配内存的局部变量.
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; }例子:
int j ; int f(){ j+=20; return j; } 上面的j就是全局变量了.全局变量可以被各个函数调用, 所以全局变量j的生存周期跟函数f()无关.
也就是说, 全局变量的生存周期是就是程序的生存周期.
即是,如果程序一直在运行, 变量j所占的内存就不会被释放. 理论上讲, 定义的全局变量越多, 程序所占的内存就越大.
可见全局变量在内存中是静态的, 一旦被分配内存.它不会被释放和重新分配. 所以它占用的内存被分配在内存里的静态区:
举个例子:
#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)); //good return p; } int main(){ int * q = f(); *q = 20; printf ("*q is %d\n", *q); free(q); q=NULL; }参考上面那个小程序.
在函数f()中.
int * p = (int *)malloc(sizeof(int));这条语句首先定义了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本身也会被释放.
由上面的例子可以看出, 动态内存与静态内存有如下的区别.
1.静态分配内存的变量用 类型名(结构体名) + 变量名 定义, 动态分配的内存变量用malloc划分, 然后必须把地址传给另1个指针.
2.静态变量在内存里的栈区(局部变量)或全局区(全局变量 or 静态局部变量(后面会提到))里分配. 动态分配内存的变量在heap区分配.
3.静态分配内存变量生命周期有两种, 其中局部变量,在函数结束后就会被释放, 而全局变量在程序结束后才释放. 而动态变量需要程序员手动释放, 否则会在程序结束后才释放.
看起来动态分配内存的变量的使用貌似比静态分配内存的变量使用麻烦啊. 单单1个malloc函数都令新人觉得头痛.
但是动态分配的内存有三个优点.
1.可以跨函数使用. 参见上面的例子, main()函数使用了f()函数定义的动态内存.
有人说全局变量也可跨函数使用啊, 的确. 但是全局变量必须预先定义(预先占用内存), 而动态分配内存可以再需要时分配内存. 更加灵活.
关于跨函数使用内存可以参考我另1篇博文:
http://blog.csdn.net/nvd11/article/details/8749395
2. 可以灵活地指定或分内存的大小.
例如 (int *)malloc(sizeof(int) * 4) 就划分了4个字节的内存(动态数组). 这个特性在定义动态数组时特别明显.
而且可以用realloc 函数随时扩充或减少动态内存的长度.
这个特性是静态分配内存的变量不具备的.
3. 动态变量可以按需求别手动释放.
虽然局部变量随函数结束会自动释放, 而动态分配的内存甚至能在函数结束前手动释放.
而全局变量是不能释放的. 所以使用动态内存比使用全局变量更加节省内存.
但是动态分配内存也有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 overflow int i; for (i=0;i < 5;i++){ // error, should be i < 4 printf("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 关键字是不能修饰动态分配内存的.
例如
int * p = static (int *)malloc(sizeof(int)) 是错误的.但是 下面写法是合法的.
static int * p = (int *)malloc(sizeof(int))上面的static 不是修饰动态分配的内存, 而是修饰静态分配的指针变量p
上面也提到过了, c语言的static 可以修饰如下三种对象:
1. 全局变量和函数
2. 函数内的局部变量.
注意, c语言结构体的成员不能用static 修饰
在全局变量和函数名前面的 static函数并不影响 对象的内存分配方式,
static 修饰的全局变量还是被分配与全局区中.
而被static修饰的函数的2进制代码还是被分配于程序代码区中.
这种情况下 static 的作用只是简单地对其他c文件的函数屏蔽.
也就是1个c文件a.c, 引用了另一个c文件b.c
那么a.c 文件就不能访问b.c 文件里用static修饰的 全局变量和函数.
如果用static 来修饰c语言函数中的局部变量, 那么这个局部变量是静态局部变量了.
如下面的例子:
#include <stdio.h> #include <stdlib.h> int f(){ int i = 1; // local variable static int j = 1; // static local variable i++; 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进制代码是存放在内存里的程序代码区的.
f()每次执行时都会在Stuck区为变量i初始化一块内存. 而结束时会自动地把该内存释放,
也就是说int i = 1; 这个语句每次执行时都会执行. 所以i每次输出的值都是一样的.
如果无, 则执行初始化语句 static int j = 1; 并记录下该内存的地址.
如果有, 则直接使用该内存.
当f() 执行完成时, 该内存不会被释放.
也就是讲, 当f()下一次执行时, 就不会执行 static int j =1; 这条语句.
所以当f()循环执行时, j的值就会递增了.
也就是讲, 当static 修饰1个局部变量时, 会更改局部变量的内存分配方式.而且这个局部变量的生命周期就会变成全局变量一样.
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进制代码.
其中全局变量在函数外定义, 内存分配在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区.
那么其生命周期就也会随函数执行完成而结束, 这里就不细讲了.
这代码静态定义了1个结构体a. 它的长度是4 + 16 =20 byte
而内存是被分配在 stuck区的. 注意, 这个时候, A里面的两个成员: id 和 name里面是垃圾值, 并没有初始赋值的. a.id = 1; strcpy(a.name, "Jack");这两个就是为结构体a的两个成员赋值了. 不多说..
图解:这种静态定义使用的结构体优点很简答: 方便使用, 安全性好.
缺点是什么呢? 当然了, 上面都提过: 1. 不能夸函数使用, 生命周期随函数结束. 2. 不能灵活释放. 其实这两个缺点都系虚的. 真正的问题是, 在生产中, 1个结构体往往定义得十分复杂. 也就是包含几十个成员, 几十个函数指针(方法). 那么这个结构体所占的内存就很客观了, 栈的内存大小有限, 而在heap区能申请更大的内存, 这个才是动态分配内存的结构体的必要性.看下面的例子:
#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 = A_new(1,"Jack");这里是关键了, 调用A_new()函数, 在heap去分配1个结构体的内存, 然后把头部地址赋给a.
那么在栈区的指针a就指向堆区的内存了.
a->A_prinf(a)这里调用了结构体对象a的函数指针A_prinft, 这个指针在初始化函数A_new()执行时已经被指向了真实函数prinft_A().所以, 实际上是调用了printf_A(). 但是,参数是必要的.
后面的就是释放内存和致空指针. 因为c语言不会自动释放动态内存.
图解:
其实单单只看这个例子main()函数的代码, 是不是觉得很像面向对象语言java 或 C++.
所以讲, 面向对象其实是1种编程思想. 而Java的内部实现还是离不开c/c++.
我们也可以在这里看出面向对象的一些特性, 这实际上也是动态分配内存的优点:
1. 对象的指针存放在栈区, 而无论1个结构体的内存占用有多么庞大, 栈区的对象指针只会保存结构体内存的头部指针.
所以栈区的单个对象指针只占用4byte. 相对于静态分配的结构体, 大大节省了栈区的空间.
2. 多个不同的对象会利用不同的heap区内存存放各种的成员(例如id, name), 但是各自的函数指针指向相同的代码区函数.
也就是说, 每1个结构体对象的内部函数实际上都是一样的 (除非手动再指向). 只不过参数不同.
而类是允许的. 而且函数的定义代码也写在类里面..
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成员操作时就会弹出错误: 对象没有实例化了, 就是这个原因.
b = new B();
接下来这一句就比较重要了.
首先 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里有自动释放的机制, 所以不必程序猿手动释放了.