重读金典------高质量C编程指南(林锐)-------第七章 内存管理
2015/12/10补充:
当我们需要给一个数组返回所赋的值的时候,我们需要传入指针的指针。当我们只需要一个值的时候,传入指针即可,或者引用也可以。
结构大致如下:
char* p = (char*)malloc(sizeof(char)*num);
while()
{
func(char** p);
}
free(p);
void func(char**p)
{
char buffer[num] = {0};
for(int i = 0; i < num; ++i)
(*p)[i] = buffer[i];
}
先介绍下程序运行的几个状态:根据变量所处的时间分类
程序编译时,载入内存时和程序执行时。
1、内存分配的三种方式
1)从静态存储区分配区域
针对于全局变量与static类型变量
2)从栈上创建
针对于局部变量,一般在函数执行完毕的时候,自动释放,栈内存分配运算内置于处理器的指令集中,效率很高
但是分配的内存容量有限
3)长堆上创建
针对于new,或者malloc这类的函数,可以动态开辟存储空间,而且这些内存空间需要我们自己去管理,记住这和地址不一样,内存空间有地址+内存组成,再到电子层面的话,就是有很多的二极管组成的一个个阵列。当我们释放内存的时候,用delete或者free来释放内存。他的生存期有我们用户去管理。而上面的从栈上创建的内存,它是由系统自己管理的。这个需要注意。
2、常见的内存错误
1)内存分配未成功,但我们却用了它
一般我们用一些判定语句来防止这些错误的发生,比如说 if(p == NULL)等
2)内存分配成功,但是尚未初始化就引用了它
首先要注意两点,使用任何一个变量最好先给它初始化一下,并不是缺省值就是0,这些需要我们自己去初始化的。比如说数组
3)内存分配成功,且已经初始化,但操作越过了内存的边界
这个也是针对数组说的,不可越界
4)忘记释放内存,导致内存泄露
要注意指针用完了之后需要进行释放,然后把资源放到内存当中去,别占着茅坑不拉屎。
5)释放了内存,但却继续使用它
我们在return时,千万不要返回指向栈内存的指针与引用,因为该内存在函数体结束时就被释放了。
当我们使用完delete或者free语句后,最后再加上一句,P = NULL,放在导致野指针。
规则:用malloc或者new申请内存后,应该立即检查指针值是否为NULL(这点提醒我们,当我们没有分配内存的时候,指针值是为NULL的,但是这要区别与在函数中定义的指针问题,请看下面的介绍:)
以32位平台为例:
char c; // 声明一个字符变量c,并为其分配一个8bit的空间,假设为0x22334455
c=41; // 将字符'A'放到刚才分配的0x22334455空间中
char *p = NULL; // 声明一个字符指针p,并为其分配一个32bit的空间,假设为0x44556677,p的值初始化为0
p=&c; // 将0x22334455赋值给p,存入0x44556677中。
p=new char; // 申请一块新的内存,大小为8bit,假设其开始地址为0x88990011,则此时0x44556677中存放的值为0x88990011
delete p; // 释放刚才申请的空间。p的值将被修改,此时0x44556677中存放的地址是未知且危险的。
p = NULL; // 将0x44556677中的值置0。
对指针的再理解(原创):
注:你每定义一个变量,程序自动给其分配一个空间,这里它是有地址和空间的,就相当于你现在有了门牌号和房间,但至于房间里面放什么,还有看你放什么东西,也就是说此变量代表着你房间里面放的东西。 char *P = NULL; 这句话定义了一个char*类型的变量P,它也是一个变量,他也有地址和房间。然后他房间里面放的是0; P = &c;意思就是说把C的这个地址值替换掉P的内容;P是一个实体,他有门牌号(地址)有房间(物理内存空间)和里面放的东西(P的值)。这句话就是将P这个实体的值改为C这个实体的门牌号。然后,我们可以通过P这个实体的值就可以在去寻找C这个实体了,它的门牌号已经告诉你了。 P = new char;给P这个实体换了一间房子,其门牌号变了,因为这是在P这间房间里面又建立了一个房子,此时呢,P这个实体的门牌号变了,之前属于P这个实体的房子,现在里面存的值是新建的P这个实体的门牌号了。也就是说,C的门牌号的地址值被覆盖了。 delete P;就是吧刚刚建立的房子搬迁到其他地方了,不在原来的地方了。但其值被修改了,但那一刻他的地址是不变的。但我们最好把这个房子归为原始的样子,我们已经够把这个P给折腾的了,是该让他享享清福。把P实体的值赋予0.
=====
因此你所认为的:
1. 声明一个指针int *p;后,就已经分配了内存
正确
2. int *p;后 &p是有实际的地址的
意思正确,表达有问题。应该说p是有
实际地址
的,&p就是这个地址,在上面的例子中为0x44556677。
3. 只是没有该内存中赋值时,编译器会自动给该指针赋值为0xcccccccc
基本正确。这是VC干的事情,在VC中基本上未初始化的指针都是这个值,但是平台不一样这个值可能会不同。因此为了自己判断的方便,闲置或初始化的指针一般都设为NULL。
- 追问:
定义一个指针:
int *p;
p = 3; // 报错,类型不匹配
但是如果这样:
int *p = new int;
delete p;
p = 3; // 正确,可以成功赋值,p成功指向0x00000003的内存
请问为什么释放指针指向的内存后,可以直接对该指针赋值,而在之前却不行呢,谢谢!
- 回答:
- int *p;
p = 3; // 3的类型是int(编译器隐式转换),而p的类型为int *(用户声明)。
大多数新版的编译器中,无论p是否经过new再delete,直接将3赋值给p都会造成类型不匹配的错误,比如VS2010。
需要注意的是,
p=(int*)0x00000003;
虽然不安全,但却是合法的,无论有没有经过new再delete。
你所指的成功赋值可能存在于部分旧版编译器,但是我无法验证,而且这并不符合C++规范。
请使用最新的编译器。
规则:需要给数组与动态内存赋初值,防止将将未初始化的内存作为右值使用。
规则:避免数组下标越界或者指针越界操作。
规则:指针的开辟与释放要成对出现。
规则:在用free与delete释放掉内存后,立即将指针置为空,防止出现野指针。
3、数组与指针的比较
一般情况下,数组在静态存储区被创建,比如说是全局数组,或者在栈上被创建。其数组名,对应着一块固定的地址
注意不是指向,其地址和容量在其生命期内不会改变,但是我们可以更改器内容。
而指针可以随时指向任意类型的内存块,它的特征是可变的,所以我们常常用指针来操作内存块,它比数组更加灵活,
但也很容易出错。
1)修改内容
比较下面的例子
char a[] = “hello”;
a[0] = ‘X’;
cout << a << endl;
char *p = “world”; // 注意p指向常量字符串
p[0] = ‘X’; // 编译器不能发现该错误
cout << p << endl;
字符数组a是一个容量为6的字符数组,其内容为hello\n。其值可以被修改。注意对于指针P,它指向的是一个常量字符串(位于静态存储区)这个常量字符串的内容不可以被修改,只能通过字符数组去修改这个值。
2)内容复制与比较
对于字符数组,我们不可以直接复制也不可以直接比较其大小。而要用string函数里面的标准库函数strcpy()和strcmp()
对于字符指针,也和这个差不多,我们开辟空间的时候可以连续开辟一整块的空间,然后进行比较。你可以把数组理解为以数组名为首地址,一段连续的存储空间。
3)计算内存容量
一般用运算符sizeof(),可以计算出数组的容量(字节数)。包括\n,这也是一个字符。
比如,char a[] = “hello world”; char *p = a;
此时sizeof(a) = 12; 而sizeof(p) = 4;
这是因为,前面的sizeof其实求的是字符数组容量,说到这突然想起,a.lengh。这是指数组的长度,不是容量,因为一些
容器类型的变量,可能除了长度外还有其他东西存在。
而下面的sizeof(p)则是指 sizeof(char *)是指的是指针变量的字节数。而不是p所指向的内存的容量,其实我们在C或C++中没有办法知道指针所指的内存容量的,除非我们在申请内存的时候记住它。
还有就是当整个数组作为函数的参数时,数组名退化为指针。比较下面的数据:
4、指针参数是如何传递内存的呢(难点)
注意一点:当我们想要开辟内存时,一定要注意到底是想要给指针所指向的值开辟空间,也就是说,我们可以拿着这些个空间可以去存这么大个长度的值。当然,它是有长度的。是存的值。
第二是,我们假如需要开辟多个同类型的指针的时候,我们需要传递指针的地址,不然怎么去开辟空间,这个里面装的可是指针也就是指针的指针。
如果函数的参数是一个指针,不要指望用该指针去申请动态内存。示例7-4-1中,
Test函数的语句GetMemory(str, 200)并没有使str获得期望的内存,str依旧是NULL,
为什么?
毛病出在函数GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针
参数p的副本是_p,编译器使 _p = p。如果函数体内的程序修改了_p的内容,就导致
参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请
了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory
并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用
free释放内存。
如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,见示例7-4-2。
由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回值来传递动态
内存。这种方法更加简单,见示例7-4-3。
用房间理论对上面问题的理解:
我们现在的目的是什么?是想在str这个实体房间里面创建一个个小的房间,然后去放变量,这些一个个小的房间,其实他的实际情况不就是一个个房间实体的门牌号。也就是说,str里面存放着一个个门牌号。???
上面这种解释是错的,一个str房间实体,一般只能存放一个门牌号的值,不能存那么多值,不然那就不叫一个变量了,变成数组了。但是我们却可以根据str房间里面的某一个门牌号的值跟踪到其他门牌号的值。然后,理所当然的存放数据了。???
还是错的。其实没那么复杂,这是想去多创建几个动态存储空间而且。是在str原有房间实体的基础上(栈中),去(堆中)开辟几个str类型的空间而已。
对于刚刚的那个图,我们传递的是str这个实体房间的值,我们试图想要创建100个如同str这个实体的复制品,虽然这么多的复制品,我们可以通过str找到和他相关联的其他实体,但我们传的是str这个实体房间的值,连门牌号都不给别人,然后试图想让别人通过你给的门牌号,找到相同类型的房间,简直是痴人说梦。最起码也要把门牌号传递过去呀。现在我们假如你传递的是str这个实体的值,然后我们通过这个值,就相当于执行了p = new char ;操作一样,把str原来的房间搬迁到里面来了,原来的房间这个实体也不知道被谁占了了。然后,原来房间的实体的值放的是str这个门牌号的值。然后返回,现在的问题是,我们在原来的房间里面又开辟了房间,而我们想要的却是开辟与str同类型的房间,然后在通过这些同类型的房间这些实体去存储某些元素的值。在这里,str原来的房间扩大了吗?其实没有。只是多建立了几个子房间而已。而str这个实体的值改变了吗?没有。它还是null.
那么我们如何改变str里面的值呢?这是我们需要传递str的地址,通过str的门牌号,我们才有可能去更改str里面的值。或者开辟一些同str相同类型的实体。这是通过更改str里面的值实现的,我们可以根据str里面的值找到其他的实体。然后,我们就可以通过这些个实体去实现我们想要实现的了。这个实现需要传递指针的指针。
第二种方法就是我们在函数体里面先去创建同str同类型的实体,然后我们将这个实体里面的值,给了str这个实体。此时,我们可以通过str这个实体里面的值,这个值是P这个实体的。P是str这样的实体房间。P是什么,P是地址,str里面放的是地址值。是门牌号。
再深挖一下P的值表示P房间里面的内容,那P这个房间里面的内容到底是什么呢?原来P房间里面的内容我们不知道是什么。但返回的是char *P,char *P是同str一样的东东。P里面存放的是系统给我们分配的值。这个就是地址值。从房间理论思考,不就是s=p;赋值操作而已。把P这个房间给了s这个房间。这个上面第一个有很大的不同,这个是重新找一个值进行赋值操作。第一个是直接对str这个变量进行操作。然后我们就可以操作了,有房间就有地方放东西了。通过赋值将s马上从栈空间变为堆空间。这种方法都不用房间里面套房间。简单,容易。
/***************************************************************************************************************/
突然想起一种新观点,基于栈与堆的区别:针对图 7-4-1, 我们在调用函数时,如果只传入指针,而C++编译器会copy一个副本,_p = p;这样的话,我们开辟char* 类型的堆内存空间,它会有一串的栈内存空间来存放你的门牌号,也就是开辟这些指针的地址,但并没有开辟栈内存空间,而是程序自己在运行的时候开辟了一串的内存空间来存放指针的地址,也就是门牌号。回归到原题,我们只传入指针并开辟了一串与str类型相同的变量,当我们执行到函数末尾,返回到主程序,此栈空间被系统回收。我们也就找不到原来在栈空间里面的地址.对于7-4-2,要想能够保存原来在主函数里面的栈空间地址,我们就需要用指针的指针去存储在main函数里面的栈空间地址。这样的话,我们在调用函数中,在开辟堆空间的同时会相应的开辟对应的栈空间的地址,且临时栈空间释放与否对于程序本身没有一点影响。
对于7-4-3 ,我们还可以利用返回指针的形式保留下调用函数当中的临时栈的地址,这样就可以找到堆空间里面的房子了。我们始终要记得:一个堆空间对应一个栈空间。
对于7-4-4,指针的引用这个和指针的指针是一样的,可以理解为它可以保存 与堆空间对应的栈空间的东东。临时栈被释放掉了。
还有一点,我们不能去用return返回栈内存空间的指针。能够返回的永远是堆内存空间的指针。因为,栈内存空间里面的指针,用完了就销毁了。没有任何意义,变成垃圾了。
对于函数里面一个指针指向一个常量字符,它是位于静态存储区,所以没有什么意义。无论什么时候调用GetString2,它返回的始终是同一个“只读”的内存块
7.5 free和delete把指针怎么了?
只是将P所执行的内存给释放掉了,就是将P的值变了。成为系统默认给定的值了。但其地址并没有发生变化,只是删了P而已。其值改变而已。门牌号还在那。最好此时给它归到原始位置。
char *p = (char *) malloc(100); 可以理解为在栈这个房间里面建立了堆房间。可能用指向更好一点。
7.6 动态内存会被自动释放吗?
不会。
7.7 杜绝野指针
野指针不是NULL指针,而是其值指向垃圾内存的指针,其值由系统指定。if语句对它不起任何作用。
7.8 为啥有了malloc/free还有new/delete
一对是C语言的,一对是C++语言的。然后,new还可以执行构造函数,delete可以执行析构函数。
malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
一般来讲,我们用new/delete来完成动态对象的内存管理,而用malloc/free来管理内部数据类型的对象。这个用前者也不会错。
用法:
P = new float[num];
delete[] p;
7.9 内存耗尽怎么办
一般用三种方式来处理
1)通过return返回。
2)通过exit(1)来将程序杀死。
3)为new和malloc设置用户自定义异常处理函数。
7.10 两对函数的使用
int *p = (int *)malloc(sizeof(int)*length);
free(p);
与上面对应的new与delete运算符:
int *p = new int[length];
delete[] p;
当然,这是某个数据类型的对象。
当我们new某个动态对象的时候,不用[]而用();
这是因为new内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,
那么new的语句也可以有多种形式。例如:
至此整个内存管理这块讲完了。总的来看其实就是几个内存空间的管理,这里面涉及到一些变量的所处的作用域,可能在栈里面也有可能在堆里面,我们需要对于不同的数据做不同的管理。