专题复习_内存
要说C语言最优秀最方便的东西是什么,我想是指针,要说C语言入门最难的是什么,我想还是指针.
原因只有一点,它是直接与内存紧密关联的东西,这个可恶的*号&号不知道难倒了多少初学C/C++的朋友,连很多老鸟也经常栽到在这里,某志今天专门抽出一些时间来整理指针和内存相关的资料,呼,令人弃之可惜,食之哽喉的邪恶物啊。要清楚内存的相关信息,首先我们先说
内存分配形式
- 静态存储区
- 堆
- 栈
其中静态存储区存储的是我们的全局变量和static变量。OK,解决了一个问题,为什么我们总是说,当我们在变量前加static则相当于扩大了其生存周期,原因就在这里。静态存储区这块内存从程序开始编译时就已经分配好,直到我们退出了这个程序,它才自动释放,在整个程序运行期间,它一直存在,所以,即使我们在一个函数中声明了一个static变量,它也是全局可用的。也正因为如此,我们尽可能的避免使用该类变量,长期的占用内存哦!
其中堆分配,又称为动态内存分配,这个就是new/malloc咯,是由我们手动来分配一块内存的,而且系统不会自动释放,这就要求我们使用free/delete来进行手动释放,所以该块内存的生命期完全由程序员控制,自由的多,所以也最容易出现各种问题。
最后栈上创建是我们最常用的,我们函数的局部变量都是在这上创建的,栈上的数据在函数执行结束的时候会自动释放。栈上内存的分配运算是在处理器的指令集中的,效率非常高,但是一点,内存容量很有限。
发生内存错误是非常令人郁闷的事情,我们Debug也很难直接的检测到它,因为我们的IDE并不能自动发现这些错误,而且这些内存错误中甚至可能会是“间歇性”的,就是说,有的时候会反映出来,有的时候它并不致命,没有体现出来。所以我们需要对这些内存错误进行人为的处理,下面我列举些经常可见的内存错误和解决方法。
- 内存分配未成功,却使用了它。
我们的内存分配并非每次都会成功的,这一点虽然不常见,却并非不可发生,特别是我们在使用malloc和new申请的堆内存,内存分配失败的可能更大,所以我们在使用内存之前,尽可能的对其进行一些防错处理if(p!=NULL)这样的判断就OK了~:)
- 内存分配成功,却没初始化就引用它。
我们应当养成一个好习惯,声明一个变量时,特别是数组,结构时,先将它赋值为空。因为系统并不是一定会将缺省值设置为0的哦,假若其中原本保存着其他的数据,没初始化就拿来使用就很可能出现不可预测的问题哦(恩,想蓝屏的朋友可以无视此条)
- 内存使用超标。
这个是最简单的了,举例就是我们分配了一个长度为40字节的内存,结果在某个循环中,不小心把下标多弄了1,结果就超标啦。当然还有字符串最后的那个”/0”哦
- 使用完了不释放。
这是某志最容易犯的错误了,T T最简单的代表就是new/malloc后不delete/free,导致每使用一次这种错误函数,就开辟一块内存,到最后导致一个结果:内存不足。所以这两套申请和释放必须配对。
- 释放了又拿来用。
我们可以想想,栈内存中局部变量是在函数结束时自动销毁,假如我们在函数外调用它,当然会失败吧,或者之后取它的值,那只会是一堆的错误随机数了。还有就是我们自己分配的动态内存,释放之后,却没有设置它的指针为NULL,结果那指针就不一定指哪儿了,也就是常说的“野指针”。
说起了内存指针,又想起了数组,也是个很麻烦的玩意,因为在C/C++中这俩东西好多地方可以通用,某志我就总认为两者是等价的,实际上,这两者是有区别的。下面就来说指针与数组的对比
- 我们先说下数组名,我想说,它不是一个指针,为什么我们调用它时候就可以访问这个数组呢?是因为系统在内部,将数组名和数组的内容捆绑起来了,它是对应着一个数组,而不是指向一个数组。它的地址和内存容量在其生命期中永远不会改变的,我们能改变的仅仅是数组名对应的数组内容信息。
而指针,它是自由的,它没有和任何东西捆绑,它想指向谁都行,我们可以随时改变它本身的内容。
要是还不明白,这里举例看看
char a[] = "duzhi";
a[0] = ‘D’;
cout << a << endl;
//----------------
char* p = "tiancai";
p[0] = ‘T’;
cout << p << endl;
从编译器来说,是不能发现错误的。运行后仅仅会报一个XXXX内存出错,无法written。现在我们来分析一下,第一段代码中,a是个数组名,它对应的数组是六个字符长度的数组,它是和该数组捆绑好的,它捆绑的数组内容我们可以随意进行改变,所以最后输出的将是一个Duzhi。而p是一个指针,它仅仅是指向一块内容为“tiancai”的字符串的常量字符串,该常量是不能改变的,所以当我们赋值 p[0]为T时,对其的写入操作是非法的,才会报上述错误。
- 根据上面的经验,我们可以知道假如我们对数组进行赋值时就不能简单的b = a;这样了,因为数组名是和数组捆绑的哦,所以我们才有了 strcpy函数,同样,判断两数组内容是否相同时,也不能直接b == a这样判断,应当strcmp。
现在我们来想一下,假如我们有数组a[]和指针p,那么p = a的话是什么意思?呵呵,并不是将a对应的数组内容给p哦,仅仅是p指向了a本身数组名的地址,假如我们想复制a中的内容给p,那么我们可以先给p分配块动态内存,再strcpy过去。
- 我们知道byte占1个字节数,也知道单精度浮点型占8个字节,那么也应该知道指针是占4个字节的吧。那么我们现在来看这个问题,
char a[] = "duzhi";char* p = "duzhi";
那么我问,sizeof(a),sizeof(p)分别为多大呢?答案分别是6和4。
因为我们需要知道a本身是一个char类型的数组,我们是在求该数组内容的长度,而sizeof(p)就相当sizeof(char*),它求得的仅仅是p这个指针本身的字节数长度,而不管它指向的内容。
- 告诉大家一个非常非常不幸的消息,尽管数组和指针已经把我们弄的晕晕的,我们还不得不记得下面这个重要的细节问题T T
当数组作为函数的参数进行传递时,该数组会自动退化为同类型的指针。OH,MyGod!不知道谁设计的这玩意,太容易出现问题了!可是我们编程的还不得不去记住它。>_<下面弄个例子看看。
void Fun( char a[1000000000] )
{
cout << sizeof( a ) << endl;
return 0;
}
按照原本的道理来说,应当输出是1000000000,但是这里由于可怜的数组名被抓去做函数参数了,所以被降级退化成了一个指针,于是我们sizeof(a)就相当于sizeof(char*),结果就成指针所占的4个字节了。
呼,就先说到这里吧,偶有点晕晕了,明天继续一起复习~^ ^V
呼,今天我们来继续昨天的C/C++中的内存相关知识的说明。昨天我们说了三点,内存的分配形式,分为静态存储区,堆,和栈。又说了我们用指针对内存进行控制时容易出现的错误和处理,还有指针和数组名之间的区别,今天我们继续内存这个话题进行下去。
恩,先说下指针做为参数进行内存传递吧。
我们首先来看个例子:
void GetMemory( char* p, int num )
{
p = (char*)malloc(sizeof(char)*num);
}
int main( void )
{
char* str = NULL;
GetMemory( str, 100 );
strcpy( str, "duzhi" );
}
整个程序代码很简单,就是先声明一个指针,再调用函数对其进行内存的分配,之后将字符串内容拷贝到该块内存中。但是结果却是失败,为什么呢?
这是因为我们将指针做为一个参数去申请动态内存了,这是不可以的,我下面说下原因。
我们需要知道,我们编译器在编译时,会将我们函数的每个参数先生成一个临时的副本,指针参数p的临时副本是_p,_p是==p的。所以当我们修改了_p中的内容时,p的内容也会被修改,这时我们的指针就可以参加运算进行传出。但是,本例中,我们仅仅是对_p申请了一块内存,相当于把_p指向的内存地址改变了,但是p是没有任何改变的,所以,即使传出p,也将依旧是NULL,它本身没指向相应的内存。指针做函数参数是完全行不通的。
呼,现在再问个事情吧,这段程序代码还有什么问题吗?对啦,我们malloc了,却没有free哦,记住,这俩是一定要成对出现的。不然的话,我们将只会分配内存,却无法释放了。
假如我们一定要用指针去申请内存怎么办呢?我们可以使用“**”这个指向指针的指针,或者这玩意你会很难掌握,那么我们可以暂时不考虑它,那么我们就用函数返回值传出也是可以的,即return一个char*类型的数就可以,我们仅仅要求的就是不要拿指针去做函数参数开空间。(补1句,别return一个局部变量的指针哦,函数完了它就消亡咯,它可是栈内存上的0 0)
野指针
恩,接下来再说说“野指针”吧。这也是个头疼的家伙,象他的名字一样,跟个野孩子一样,到处乱串门的家伙,我们完全不知道它会指向哪儿,所以当我们使用它时,会出现什么样的错误是不可预料的。
我们开始有提过,malloc,new完了记得去释放free,delete,实际上,这些东西并不是释放指针,本来就是咯,释放指针有什么用?我们需要释放的是指针指向的分配的内存。
但是我们需要注意的是此时指针P指向的地址已经被释放了,而指针指向的地址还是不变的,只是其中的内存数据却是不确定的,我们不知道系统将用这块内存存放什么东西了,此时p就成了“野指针”,若我们不将p指向NULL,那么我们既无法合理使用该指针,当我们使用此指针时就很容易出现不可预料的错误。
当我们的程序代码较长时,有些程序员喜欢使用if(p!=NULL)来判断p的合法性,而实际上这样是不全面的,因为我们的p并非为NULL,它指向着一块地址,仅仅是我们不知道其中会是什么内容罢了。要预防野指针,我们在可能生成野指针时记得将其赋值为NULL就可以了。
“野指针”的另外一种可能生成原因还有指针生成时未初始化。我们创建一个指针变量,刚开始时候它是随机值的,我们不清楚它会指向谁,所以,在指针变量创建时一定记得初始化,让它指向一个合法内存或NULL。
“野指针”还有一种存在原因是,它操作的变量作用范围不正确,被提前释放了,那么它指向内存地址中内容也是不确认的。我们看下下面的例子
class A
{
public:
void Func(void){ cout << "Func of class A" << endl; }
};
void Test(void)
{
A *p;
{
A a;
p = &a;
}
p->Func();
}
我们这里需要注意的是a的生命周期,我们的p在调用函数时,对象a已经消失,而此时p是指向a的,则此时的p就成了“野指针”,再使用它进行方法调用,很容易出现错误。
恩,“野指针”说完了,记得出现野指针的三种可能吗?接下来我们来说下new/delete和malloc/free的区别和注意事项。
其实这里用一句话来说就够了,new/delete会默认的分别调用构造函数和析构函数,而malloc/free则是简单的分配一块内存区,不会自动调用这两函数。所以我们在使用时配对不能错误,new对应的变量就要delete去释放,free的话就可能出错,反过来一样。
我们还需要记得的是new创建对象时,调用的仅仅是该对象的无参数构造函数,假若有重载的多参数构造函数,是不会自动调用的。类似与malloc的sizeof()这些命令在new中其实也是有的,仅仅是被封装了起来,添加了一些安全的检查功能而已。
我们知道,在我们使用malloc,new要求分配一块内存时,并非是一定成功的,其中就有可能是内存资源耗尽,此时,系统就会返回一个NULL指针,宣告我们的内存分配申请失败。此时我们需要在内存分配时,加一个指针的判断,当失败时,退出程序并给出一些消息框告诉用户这一信息。这里给段代码,是专门“吃”系统内存的,我们可以想想,在最后加出的判断如果去掉的话会怎么样?
void main
{
float *p = NULL;
while( true )
{
p = new float[ 100000 ];
cout << "Eat memory" << endl;
if( p == NULL )
exit( 1 );
}
}
最后的一些话:指针真的是非常方便好用的东东,我们不要因为恐惧出错而避免使用它,只要多进行实时检测,就能轻松应对了。:)