从内存分配角度分析c和java里的static 关键字.
2013-12-30 00:05
537 查看
即使作为Java的初学者, 对this 和 static 这两个关键字都不会陌生. 其实也不难理解:
this 关键字: 指的是对象的本身(注意不是类本身) 跟.net 语言的Me 关键字类似.
static 关键字: 静态分配的对象或对象成员. 也就是指被static 修饰的成员只属于类本身, 而不会想其他成员一样会对每个对象单独分配.
但是c语言也有static关键字, 但是c语言中的static并不只是静态分配的意思,如果用在静态局部变量(函数内部), 则是说明这个变量是静态的, 如果用在全局变量或函数, 则是防止函数或全程变量被其他c文件中的函数访问(通过include 头文件). 为什么Java里的static 会跟c 语言里的有这种区别呢.
下面会从内存分配的角度浅析一下这个问题.
所以Java里有些特性和概念, 通过C语言分析能更好的理解.
首先, 1个由C语言编译的程序所使用的内存大概分成如下几个部分
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。
3、全局区(静态区)(static) --- 用来存放全局变量和静态局部变量.
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。
下面是大致的图解.
2.1 静态分配内存
对于C语言来讲, 静态分配内存就是只用 类型 + 变量名 定义的变量. 不管这个变量是局部变量(函数中) 或全局变量(函数外).
2.1.1 局部变量
第一种情况, 函数中静态分配内存的局部变量.
如上面那个简单的例子, 在f()函数里定义的局部变量j 就是1个静态分配内存的变量(注意不是静态变量).
这种静态分配的变量的生存周期就是函数执行一次的周期.
什么意思呢, 就是当f()执行时, 操作系统会为变量j 分配1个字节(32位系统)的内存, 但是当f() 执行完时. 变量j所占的内存就会被释放. 可以操作系统用作它用.
也就是说, 当f() 被循环执行1万次, 程序并不会额外占用9MB多的内存, 因为j所占的内存会不断地释放分配.
上面提过了, 局部变量所占的内存是分配在内存里的Stuck(栈)区的. 因为j是int 类型, 所以它会在stuck区占用1字节
注: 局部变量还有另1种形式就是函数的参数:
如下面的变量i也是局部变量:
例子:
上面的j就是全局变量了.
全局变量可以被各个函数调用, 所以全局变量j的生存周期跟函数f()无关.
也就是说, 全局变量的生存周期是就是程序的生存周期.
即是,如果程序一直在运行, 变量j所占的内存就不会被释放. 理论上讲, 定义的全局变量越多, 程序所占的内存就越大.
可见全局变量在内存中是静态的, 一旦被分配内存.它不会被释放和重新分配. 所以它占用的内存被分配在内存里的静态区:
举个例子:
参考上面那个小程序.
在函数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本身也会被释放.
1.静态分配内存的变量用 类型名(结构体名) + 变量名 定义, 动态分配的内存变量用malloc划分, 然后必须把地址传给另1个指针.
2.静态变量在内存里的栈区(局部变量)或全局区(全局变量 or 静态局部变量(后面会提到))里分配. 动态分配内存的变量在heap区分配.
3.静态分配内存变量生命周期有两种, 其中局部变量,在函数结束后就会被释放, 而全局变量在程序结束后才释放. 而动态变量需要程序员手动释放, 否则会在程序结束后才释放.
但是动态分配的内存有三个优点.
1.可以跨函数使用. 参见上面的例子, main()函数使用了f()函数定义的动态内存.
有人说全局变量也可跨函数使用啊, 的确. 但是全局变量必须预先定义(预先占用内存), 而动态分配内存可以再需要时分配内存. 更加灵活.
关于跨函数使用内存可以参考我另1篇博文:
http://blog.csdn.net/nvd11/article/details/8749395
2. 可以灵活地指定或分内存的大小.
例如 (int *)malloc(sizeof(int) * 4) 就划分了4个字节的内存(动态数组). 这个特性在定义动态数组时特别明显.
而且可以用realloc 函数随时扩充或减少动态内存的长度.
这个特性是静态分配内存的变量不具备的.
3. 动态变量可以按需求别手动释放.
虽然局部变量随函数结束会自动释放, 而动态分配的内存甚至能在函数结束前手动释放.
而全局变量是不能释放的. 所以使用动态内存比使用全局变量更加节省内存.
1. 就是必须手动释放.... 否则会造成内存泄露.
其实上面都讲过了, 这里举个具体例子:
程序1:
程序1的f() 函数被main()函数循环执行了100次, 所以f()在内存heap区划分了100次动态内存, 但是每一次f()结束前都会用free函数将其手动释放.
所以并不会造成内存浪费. 这里再提一提, free(p) 这个函数作用是释放p所指向的动态内存, 但是指针p的值不变, 仍然是那个动态内存的地址. 如果下次再次使用p就肯定出错了.所以保险起见加上p=NULL, 以后使用p之前,也可以用NULL值判断它是否被释放过.
程序2:
上面就是反面教材. 如果没有手动释放, 当f()函数被循环执行时就会多次划分动态内存, 导致可用内存越來越少. 也就是程序所占的内存越來越多, 这就是传说中的内存泄露.
可能有人认为, 不就是加多1个free()函数吗? 算不上缺点.
但是有时候程序猿很难判断一个指针该不该被free() 函数释放..
程序3:
看看上面的例子,
指针p和q指向同1个动态内存.
当执行free(p)时, 释放的是动态内存, 而不是释放p本身, 所以再此执行free(q)就出错了, 因为那个动态内存已经被释放过了嘛..
有人觉得, 这个错误也不难发现嘛.., 小心点就ok了.
首先, 上面的代码编译时并不会报错, 至少gcc会编译通过, 执行时才会出错...
而且, 当项目越來越复杂时, 可能有多个指针指向同1个动态内存, 而且某个指针指向的动态内存是其他程序员在其他函数内分配的..
这时你就很难判断了, 如果不释放怕造成内存泄露, 如果释放了, 别的程序猿不知道的话再次使用...就会出错.
所以有时候在项目中程序猿很难判断1个指针该不该释放啊... 特别是多个程序猿合作的大型c项目.
这就是为什么说c语言功能强大, 但是不适合编写大型项目的原因之一, 需要程序猿有相当扎实的内存管理能力.
2. 另个硬伤就是内存溢出.
什么是内存溢出呢, 就是使用了动态分配内存长度之外的内存...
举个例子:
上面定义了长度为4的连续内存空间. (动态整形数组)
但是这个程序却使用了长度为5的内存空间, 也就是这个动态内存后面额外的的那一个字节的内存被使用了.
其实就是p+4 这个地址的内存并没有定义, 但是却被使用, 这就是传说中的内存溢出.
这个代码可以被正常编译, 可怕的是, 很多情况下它会正常执行...
但是如果p+4刚好被这个程序的其他变量或其他程序正在使用, 而你却往它写入数据, 则可能会发生导致程序漰溃的错误...
这就是有些c \ c++ 程序不够健壮的原因, 有时候会发生崩溃..
所以说c语言很难就难在这里, 内存管理啊.
首先, c的static 关键字是不能修饰动态分配内存的.
例如
是错误的.
但是 下面写法是合法的.
上面也提到过了, c语言的static 可以修饰如下三种对象:
1. 全局变量和函数
2. 函数内的局部变量.
注意, c语言结构体的成员不能用static 修饰
static 修饰的全局变量还是被分配与全局区中.
而被static修饰的函数的2进制代码还是被分配于程序代码区中.
这种情况下 static 的作用只是简单地对其他c文件的函数屏蔽.
也就是1个c文件a.c, 引用了另一个c文件b.c
那么a.c 文件就不能访问b.c 文件里用static修饰的 全局变量和函数.
如下面的例子:
上面的f() 函数i就是 一般的局部变量了, 而 j 前面有static修饰, 所以j是1个静态局部变量.
如果上面的f()函数被连续执行10次, 那么 i 和 j的值是不同的.
可以见到, 每次f()执行, i 的值都是2, 而 j 的 值 会不断加1.
原因就是static 用在局部变量前面就会改变该局部变量的内存分配方式.
上面说过, 一般局部变量是放在内存Stuck区的, 而静态局部变量是放在全局(静态)区的.
图解:
当程序执行时, f()作为1个函数, 它的2进制代码是存放在内存里的程序代码区的.
也就是说int i = 1; 这个语句每次执行时都会执行. 所以i每次输出的值都是一样的.
如果无, 则执行初始化语句 static int j = 1; 并记录下该内存的地址.
如果有, 则直接使用该内存.
当f() 执行完成时, 该内存不会被释放.
也就是讲, 当f()下一次执行时, 就不会执行 static int j =1; 这条语句.
所以当f()循环执行时, j的值就会递增了.
也就是讲, 当static 修饰1个局部变量时, 会更改局部变量的内存分配方式.而且这个局部变量的生命周期就会变成全局变量一样.
而它们的区别如下:
1. 全局变量在程序开始时就分配内存, 而静态局部变量在对应函数第一次执行时分配内存.
2. 全局变量能被各个函数访问, 所以一般用于传递数据. 静态局部变量只能被定义它的函数访问, 一般用于保存特定数据.
所以说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个类的成员了.
如下面这个例子:
其中, 变量j是A的1个成员, 而不是全局变量.
而变量i 跟c一样, 是属于函数f的1个局部变量.
java里局部变量的内存方式跟c语言是一样的, 都是属于静态分配, 内存被分配在stuck区.
那么其生命周期就也会随函数执行完成而结束, 这里就不细讲了.
Class A里面的变量j 其实就是A的一个成员, 而Java里1个类里面的所有非Static修饰的成员都是动态分配内存的.
也就是讲, 其实那个变量j是动态分配内存的, 内存被分配在heap区里.
怎么讲呢, 还是要借助c语言:..
大家都知道c语言里具有类的雏形---> 结构体. 其本质就是让不同的数据类型集合存储.
首先看看结构体的静态分配内存用法:
上面的简单例子, 定义并使用了1个结构体A.
下面1句1句从内存分配的角度详细讲解...
首先:
这几句代码定义了结构体A的结构, 包括1个整形和1个字符数组的两个成员.
在程序执行过程中, 定义代码会作为2进制代码存放于内存里的代码区中.
这代码静态定义了1个结构体a. 它的长度是4 + 16 =20 byte
而内存是被分配在 stuck区的.
注意, 这个时候, A里面的两个成员: id 和 name里面是垃圾值, 并没有初始赋值的.
这两个就是为结构体a的两个成员赋值了. 不多说..
图解:
缺点是什么呢?
当然了, 上面都提过:
1. 不能夸函数使用, 生命周期随函数结束.
2. 不能灵活释放.
其实这两个缺点都系虚的.
真正的问题是, 在生产中, 1个结构体往往定义得十分复杂. 也就是包含几十个成员, 几十个函数指针(方法).
那么这个结构体所占的内存就很客观了,
栈的内存大小有限, 而在heap区能申请更大的内存, 这个才是动态分配内存的结构体的必要性.
上面就是动态分配内存的结构体最常用的用法..
再一句一句来:
这段定义了1个结构体A, 跟之前例子不同的是只不过, 多了1个函数指针, 用于打印这个结构体的成员.
这段2进制代码一样存放子在代码区中.
上面是打印函数的定义了, 我们会将结构体A的指针指向这个函数.
也会放在代码区中.
A_new()函数相当于1个初始化函数.
无论静态或动态定义1个结构体之后, 只会在stuck区或heap区分配该内存. 而内存里结构体的成员是垃圾数据().
也就是说,定义1个结构体A的"对象"b后, b的id, name, 函数指针A_prinft都是垃圾数据.
如果不经初始化, 直接使用成员, 例如函数指针的话, 系统就出错了.
所以初始化函数最重要的作用就是把每1个 对象的函数指针指针向正确的函数. 这个就是初始化函数的必要性.(当然你也可以在main函数内手动指向).
这里顺便加两个参数, 把id和name也初始化了.
注意, 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个结构体对象的内部函数实际上都是一样的 (除非手动再指向). 只不过参数不同.
Java 是完全面向对象语言, 所以任何东西都必须用类来描述. 而Java的类实际上是由c语言的结构体扩展而来的. 而上面说过, 生产环境中的类往往非常复杂.
所以 Java 里的类都是动态分配内存的.
看下面这个简单例子:
上面的例子定义了两个类,
在A的入口函数中, 定义并实例化了类B.
下面讲下, 类B是如何被分配内存的.
上面就是类B的定义代码, 跟c语言的结构体定义代码一样, 它也会被转成2进制代码而存放在代码区中.
跟c语言的结构体代码做下对比:
实际上类与结构体成员的定义都是类似的.
但是 结构体只是一个简单的不同类型的集合体. 里面的成员(包括函数指针)不允许具有初始值.
而类是允许的. 而且函数的定义代码也写在类里面..
我们看看这四句在入口函数的代码.
这个语句在c语言中可以理解成 静态定义1个B的结构体对象b.
但是在Java中, 我们应该理解成为定义1个类B的指针b, 注意java里虽然取消了指针操作, 但是java里的底层很多东西都还是需要指针来实现.
相当于c 语言里的
所以这一句理解为简单地定义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实际上就指向了heap区的对应内存.
这时我们就可以对对象b的成员进行操作. 也就是对象b已经被实例化.
所以其实实例化的真正意思就是1个对象指向了heap区的对应内存.
如图:
这时, 我们看看对象b的成员i, 它随着对象b的内存, 同样被分配在heap区里面.
所以我们就说 在函数外定义的非static变量不是全局变量, 而是类的成员, 它们是动态分配的.
既然new出来的东西是动态分配, 那么就需要手动释放? java里有自动释放的机制, 所以不必程序猿手动释放了.
看看上面这个经过简单修改过的例子.
B增加1个静态成员j, 1个静态函数g();
接下来从这个例下分析下java静态成员的一些特性.
这种情况其实就是把 成员j的内存分配在了java的数据区中, 而不是heap区, 这个内存地址在程序执行时是不变的. 而且在代码区的类定义代码里保留了这个地址(指针)
其实因为对象b里面的静态成员j指针还是指向static 区的同1个内存地址.
也就是说类B的多个不同对象实际上共享同1个静态成员.
那么java里实例化1个具有静态成员的对象时, 会同时把静态成员的地址放入栈区.
因为成员i必须在实例化之后才会被分配内存啊.
这个就是java不允许静态函数调用非静态成员的原因!
看看上面里的例子.
非静态函数f() 是不带参数的.. 而且里面的成员i 也不带参数.
再看看c语言的结构体定义:
里面的函数指针必须带参数.
其实在java里面, 非静态函数已经隐藏了1个自带参数"this".
因为上面说过了, 所有不同的实例化后的对象里面的成员内存都是不同的, 但是它们的函数都是指向代码区同1个函数.
那么调用这个函数时, 这个函数必须知道到底是哪个对象调用它, 在这里就是到底要输出哪个对象的成员i.
所以,
在底层里, 我任务非静态函数自带参数"this"这个隐藏指针.
但是静态函数不同, 因为它不允许使用非静态成员. 就无需"this"了.
完...
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)); //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本身也会被释放.
2.3 动态分配内存和静态分配内存的区别.
由上面的例子可以看出, 动态内存与静态内存有如下的区别.1.静态分配内存的变量用 类型名(结构体名) + 变量名 定义, 动态分配的内存变量用malloc划分, 然后必须把地址传给另1个指针.
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 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关键字对内存分配的影响.首先, 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 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进制代码是存放在内存里的程序代码区的.
对于变量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 = 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个结构体对象的内部函数实际上都是一样的 (除非手动再指向). 只不过参数不同.
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成员操作时就会弹出错误: 对象没有实例化了, 就是这个原因.
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里有自动释放的机制, 所以不必程序猿手动释放了.
六. 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"了.
完...
相关文章推荐
- 从java内存分配角度分析android内存泄漏问题
- Java多线程--使用Java Mission Control进行内存分配分析
- JAVA基础-栈与堆,static、final修饰符、内部类和Java内存分配
- java内存泄漏原因(从内存角度分析,而不是从语法角度)
- java内存分配分析/栈内存、堆内存
- 在Java中,关于Static关键字的案例分析
- 源码分析:Java对象的内存分配
- java内存分配分析/栈内存、堆内存
- java内存分配分析/栈内存、堆内存
- (转)java内存分配分析/栈内存、堆内存
- static关键字的使用与内存分析
- 深入理解java虚拟机(六):java垃圾收集分析实战(内存分配与回收策略)
- 源码分析:Java对象的内存分配
- 【java学习】java单例设计模式、内存分配、性能分析
- java内存分配分析/栈内存、堆内存
- java 字符串内存分配的分析与总结(推荐)
- static关键字内存分析
- static、final修饰符、内部类和Java内存分配
- java中static 内存分配
- java的Static关键字使用内存图解: