指针和内存

来源:互联网 发布:java安装环境变量设置 编辑:程序博客网 时间:2024/06/03 16:01

指针和内存

2017-04-26


为什么要用指针

指针解决两类软件问题。第一,指针允许代码的不同部分简单地共享信息。前后复制信息可以达到同样的效果,但是指针能够更好地解决问题。第二,指针支持复杂关联数据结构比如列表和二叉树。

指针是什么

指针存储某一个值得引用而非值本身。

指针解引用

解引用操作在指针引用之后,目的在于获取指针数据的值。当解引用操作正确使用时很简单,即获取指针数据的值。唯一限制在于指针必须有指针数据来解引用。在指针编码中几乎所有的bug涉及破坏这一限定。在解引用生效之前,指针必须分配指针数据。(即指针必须有所指向)

空指针

常量NULL是特殊指针,即什么也不指。NULL为设置没有指针数据的指针提供了便利。解引用空指针属于运行时错误。NULL和整型常数0等价,因此可以当作逻辑非使用。正式C++不再使用NULL符号常量——直接使用整型常量。Java中使用符号null。

指针分配(赋值)

两个指针之间相互赋值会使它们指向同一个指针数据。对于某些潜在的复杂情况,指针间相互赋值会带来很大便利,指针间的相互赋值不会改变指针数据的值,仅改变指针指向的指针数据。赋值操作同样对NULL值有效。用NULL指针赋值操作将把NULL值从一个指针传递给另外一个指针。

画图

内存图示是考虑指针代码的关键。当你在看代码的时候,想一想它在运行时是如何利用内存的,很快画一张图表达你的想法。

共享

指向同一个指针数据的两个指针称为“共享”。在任何计算机语言中,两个或多个实体共享同一个存储结构是指针的主要优势。指针操作仅仅是技巧,共享才是真正目的。共享可以用来在程序不同部分提供高效通信。、

浅拷贝和深拷贝

特别的,共享可以支持两个函数之间的通信。一个函数传递指向兴趣值的指针给另外一个函数。二者均可获得兴趣值,但兴趣值本身并未被拷贝。这类通信称为“浅拷贝”,因为是一个(小的)指针被传递使得兴趣值共享而非对兴趣值的一个(大的)拷贝。接收方需要明白他们拥有浅拷贝,因此他们知道不要改变或删除该数据,因为它是共享的。被完全复制和发送的可选值称为“深拷贝”。某种程度上,深拷贝更简单,但由于是全复制,深拷贝运行速度稍慢。

坏指针

当一个指针首次分配时,它并不对应(指向)指针数据。该指针时“未被初始化”的,或者简单来说,是坏的。对一个坏指针进行解引用是非常严重的运行时错误。如果你很幸运,该解引用操作会立马崩溃(Java就这么运行)。如果你很不幸,坏指针的解引用会侵占内存中一块随机区域,稍微转变程序操作,最终导致在之后的某个不确定时间段内崩溃。在支持解引用操作之前,所有指针必须指向指针数据。在指向指针数据之前,指针为坏指针,禁止使用。

坏指针非常普遍,事实上,每一个指针都是以坏值开始。正确的代码用对指针数据的正确引用复写坏值,此后指针正常工作。指针不会自动赋予合法值。

划重点

很多语言为了简便省略了这关键一步,编程要小心。如果代码崩了,坏指针是第一个值得怀疑的。

指针在动态语言如Perl,LISP,Java等中有些不同。当分配指针时运行时系统把指针指向NULL,每次解引用都重新检查合法性。因此代码依然能够显示指针bug,但它们将会优雅地停止在警告区,而不是像C一样随意崩溃。也因此,在动态语言中,定位和修复指针bug更容易。运行时检查也是为什么这类语言总是会比编译语言如C或C++慢一些的原因。

两个层面

指针层面和指针数据层面,两个层面均需要初始化和连接以工作。

  1. 分配指针
  2. 分配指针数据
  3. 将指针指向指针数据

指针类型语法

int* 类型: pointer to int
float* 类型: pointer to float
struct fraction* 类型: pointer to struct fraction
struct fraction** 类型: pointer to struct fraction *

指针变量

变量声明给新变量类型和存储空间来存储值。声明不会将指针指向指针数据,即声明的是坏指针。

&操作符(取地址操作符)

有很多种方法获取指针数据的引用,最简单的就是&取地址操作符。

使用&有可能编译通过但在运行时出错。

*操作符(解引用)

指针必须对应指针数据,否则是一个运行时错误。

分配指针不会自动赋值,需要手动将一个具体值的引用赋值给指针,这是经常忘记的一个独立操作。

指针在机器中是怎样执行的

简而言之,内存中的每一块区域都有如1000或者20452这样的数字地址。指针即地址。解引用操作即看一下指针中存储的地址,然后到相应的地址中将指针数据提取出来。NULL值通常就是数字地址0.计算机从不给0地址赋值,因此该地址可被用来表示NULL。一个坏指针事实上就是包含了随机地址的指针——就像是未被初始化的int变量以随机int型变量开始一样。该指针还未被分配具体指针数据的引用。这也是为什么用坏指针进行解引用操作如此不可预测。

词条“引用”

“引用”和“指针”意思相近,区别在于,”引用“往往用作讨论指针问题,不针对某一语言或实现。“指针”暗指C/C++作为地址的指针实现。

为什么坏指针如此普遍

思维定势。简单变量不需要额外设置,声明之后即可直接用,比如int,char,struct fraction等等。不幸的是,指针并不是简单变量,在使用之前需要额外初始化。

第二部分——局部存储

内存的分配和释放

变量代表计算机内存的存储空间。并不是程序中的每个变量都有固定分配的内存。术语讲,当变量拥有一块内存存放它的值时,称为分配。当系统从变量回收内存空间时,称为释放,此时它不再有存储值的空间。对于变量而言,从分配到什邡的这段时间称为变量的生命周期。

内存最普遍的错误使用便是使用已释放的变量。对于局部变量,现代语言自动防止该错误。对于指针,程序员必须确认分配处理正确。

局部存储

局部变量是最普遍的变量。

变量被称为是“局部”即表示它们的生命周期和函数绑在一起。随函数生而生,死而死。

参数和局部变量的唯一区别在于,参数传自调用者而局部变量开始于随机初始化值。

局部变量的优点

  1. 方便——局部变量满足快捷使用,函数通常需要临时内存,仅需要在函数计算范围内有效。局部变量便捷地提供了这类临时、独立内存。
  2. 有效——相比于其他内存使用技巧,局部变量非常有效。分配和释放时效性高(快速),空间效率高,可回收。
  3. 局部复制——局部参数基本都是从调用方拷贝而来,这也通常称为“值传递”。参数是从调用者赋值拷贝而来的局部变量。调用者不和指针那样“共享”参数值,而是得到自己的副本。

局部变量的缺点

生命周期短——可以用堆解决;

限制性通信——由于局部变量是调用参数的副本,它们不提供从被调用者到调用者的通信,这便是“独立性”优点的负面影响

“局部”的同义词

局部变量也被称为“自动”变量,因为它们的分配和释放作为函数机制的一部分。局部变量有时也被称作“栈”变量,因为从底层来讲,各类语言通常通过内存栈实现局部变量。

// TAB -- The Ampersand Bug function// Returns a pointer to an intint* TAB() {int temp;return(&temp); // return a pointer to the local int}void Victim() {int* ptr;ptr = TAB();*ptr = 42; // Runtime error! The pointee was local to TAB}

局部变量总结

要解决的问题

一个函数如何将数据反馈到它的调用方?

一个函数如何在少些生命周期限制情况下分配独立空间

函数调用栈如何工作

引用参数

在最简单的“值传递”或者“之参数”方法中,每个函数有独立的局部内存,函数发生调用时,参数从调用者到被调用者拷贝。但是从被调用者到调用者如何通信?在被调用函数底部使用“return”拷贝结果将其传回调用者,这种方法对一些简单的情况适用,但是一些复杂的情况却不行。有时来回复制值并不可行。“引用传递”参数解决了所有的问题。

“兴趣值”指调用者和被调用者之间想要传递的值。引用参数传递兴趣值的指针而非兴趣值的副本。这种方法利用了指针的共享性,因此调用方和被调用方可以共享兴趣值。

语法

C语言中,引用参数的语法即对指针操作

A person with one watch always knows what time it is. A person with two watches is never sure.

避免复制。

关于&Bug

使用&从调用者到被调用者传递指针变量到局部存储没问题,反过来,从被调用者到调用者,便会出现&bug,因为函数一旦退出,所占内存便会被释放掉,指针随之失效。

**的情况

要是兴趣值已经是一个指针,比如int* 或者 struct fraction*?这样会改变设置引用参数的规则吗?并不会。引用参数仍然是一个指向兴趣值的指针,几遍兴趣值本身便是指针。假设兴趣值是int .这意味着兴趣值本身便是int 值,是调用者和被调用者所共享的。所以引用参数应该为int**.对引用参数的单个 解引用操作和之前一样可以获得兴趣值。两个( *)指针参数在链表中很普遍。

堆内存

堆内存也称为动态内存,是对局部栈内存的替代。局部内存太自动化,堆内存不同,程序员为“块”内存申请特定大小的内存分配,这个块会一直存在,直到程序员明确申请释放空间。没有什么是自动完成的。因此程序员堆内存有更大的支配权,但也有了更大的责任,因为内存现在必须主动管理。

堆内存的优点在于:

生命周期——由于程序员现在能够控制内存的分配和释放,在内存中建立数据结构,并将数据结构返回给调用者。这在局部内存中是不可能实现的,因为函数退出时内存被自动释放。

大小——分配内存的大小可以用更多细节来控制。比如,字符串缓冲可以在运行时分配,可以恰好是要容纳字符串的大小。而在局部内存中,代码更倾向于尽可能大的缓冲大小以保证最好的效果。

堆内存的缺点在于:

工作量变大——堆分配需要在代码中作出详尽的安排,工作量变大;

bug变多——由于现在内存分配需要手动完成,有可能的误操作会导致内存bug。局部内存有约束性,但至少永远不会发生错误。

尽管如此,很多问题只能用堆内存解决。在有垃圾回收器的编程语言中,比如Perl,LISP,或者Java,上边的缺点很大程度上被忽略。垃圾回收器接管了很多堆管理的责任,在运行时花费一些额外的时间来处理。

堆像什么

堆是内存中可供程序使用的很大一块内存区域。程序能够在申请内存区域或内存块。为了能分配某大小的内存块,程序通过调用堆分配函数作出明确请求。该分配函数在堆中预留出请求大小的内存块并返回指向该内存块的指针。假设一个程序为了存储三张独立GIF图像在堆中作出三次内存分配请求,每张图1024 byte.三次请求过后,内存可能是这样:

heap

每次分配请求在堆中为请求大小分配连续的区域,为程序返回指向该区域的指针,块经常扮演指针数据的角色,程序总是通过指针堆堆块进行操作。堆块指针有时被称作“基地址”指针,因为按照规定,它们指向块的基部(最小的地址字节)

上例中,这三个内存块自堆的底部开始连续分配,每个块都是按请求1024字节的大小。事实上,堆管理可以在堆中任意位置开始分配,只要块没有重叠,至少是连续的申请大小。特殊情况,一些堆区域已经分配给了程序,因此它们是“使用中”。堆管理满足每一个来自分配要求自由内存池,更新私有数据结构用一级库堆中的哪些区域正在使用中。

释放

当程序结束使用内存块时,需要给堆管理作出明确的释放请求:程序已结束对块的使用。堆管理更新它的私有数据结构来显示被占用的那片区域已经可以重新使用了。

释放掉之后,指针继续指向已被释放的块。程序不可获取该处的指针数据。指针还在,但是不能用了。有时代码会设置指针指向NULL,一旦内存释放,明确已经不合法。

编程堆

在大多数编程语言中,编程堆都看起来非常相似,基本特点是:

堆是一片可供程序分配内存区域或者内存块的内存区域;

有些“堆管理”库代码为程序管理堆。程序员向堆管理作出请求,堆管理反过来管理堆的内部构件。在C中,堆由ANSI库中的 malloc(), free()和 realloc()函数管理。

堆管理使用自己的私有数据结构跟踪堆中的哪些块能用了,哪些块正在用,这些块有多大。最初,所有的堆都是可用的。

堆可能是固定的大小(通常的构想),或者看起来是固定大小,事实上背后有虚拟内存在支撑。不管哪种情况,堆都可能变满如果它的内存都被分配出去,此时它不能响应新的分配请求。分配函数以某种方式将此时的运行时环境传递给程序——通常情况下通过返回NULL指针或者抛出一个具体的运行时异常。

分配函数在堆中请求某一具体大小的块。堆管理选择一块内存区域满足该请求,在它自己的数据结构中标记该区域正在使用,返回指向该堆块的指针。这个块保证预留给调用函数单独使用——堆不会将同一块内存区域分配给其他的调用函数。该块不会在堆内周围移动——一旦分配,地址和大小便固定了。通常,一个块被分配,它的内容是随机的,新的所有者有责任使这块内存有意义。有时,在内存分配函数上有变量设置该块为全0;

释放函数是分配函数的相反面。程序作出单一释放调用以返回一块内存到堆空闲区以便重新利用。每一个块应该只释放一次。释放函数的指针须和分配函数的一致,即为分配函数返回的指针,而不是指向该区域的任何指针。释放之后,程序必须将该指针作为坏指针处理,不允许获取指针数据。

C实现

C语言中,申请堆的库函数是malloc()和free()。这些函数的原型在< stdlib.h >中。尽管不同语言语法不同,malloc()和free()在所有语言中的角色基本一致。

The C operator sizeof() is a convenient way to compute the size in bytes of a type —sizeof(int) for an int pointee,sizeof(struct fraction) for a struct fraction pointee.

传递给free()的指针必须恰好是先前由malloc分配的,而不是一个指向块的某个地方的指针。用错误的指针调用free是常见的崩溃错误。对free()的调用不需要提供堆块的大小——堆管理会在它的私有数据结构中记录。如果程序正确释放所有分配的内存,之后每个malloc()调用之后都会恰好对应一个free()调用。实际问题是,对于一个程序来讲,释放每个分配的块不总是必要的,见下方“内存泄漏”。

heap字符串观察结果

StringCopy()包含堆内存两个重要的优点:

大小——StringCopy可以在运行时指定用来存储字符串的块的大小,在调用malloc()函数时。局部内存不能这样做,因为它的大小在编译时已经被指定好了。

The call to sizeof(char) is not really necessary, since the size of char is 1 by definition.

生命周期——StringCopy()分配块,但之后将所有权传递给调用者。如果不调用free(),块将一直存在,计时函数退出。局部变量做不到。调用者需要仔细看好内存的释放当字符串使用完成时。

内存泄漏

如果内存被堆分配却没有释放,会发生什么?一个分配了却忘了释放内存的程序可能有也可能没有严重的问题。结果会是:一直在申请,直到没有可用的内存空间。对于一个正在运行,计算的程序,之后马上退出,内存泄漏通常不是所要关心的问题。这样“一次性”的程序大多数情况下可以忽略掉所有的释放依然能够很好地运行。内存泄漏通常发生在一个不确定结束时间的程序上。这种情况下,内存泄漏会慢慢充满堆直到分配申请不能得到满足,程序停止工作或者直接崩溃。许多商用程序存在内存泄露的问题,因此当运行了很久之后,或者有很大的数据集,充满堆进而崩溃。通常情况下针对满堆的错误检测和预防代码没有很好地测试,很多是由于跑几次程序这种情况很少遇到——也就是为什么满堆通常导致直接崩溃而不是友好错误信息。许多编译器有“堆调试”功能,将调试代码加进去以追踪每一次分配和释放。当分配没有释放,就是一次泄露,堆调试会帮你找到它们。

所有权

StringCopy()分配堆块,但是它没有释放。这也是为什么调用者可以使用新字符串的原因。然而,这也意味着释放操作需要程序员完成,StringCopy()不管。也因此在StringCopy()的说明中详细说明调用者拥有块的所有权。每一个内存块有一个确切的所有者负责释放。其他实体可以拥有指针,但是他们仅仅是共享。只有唯一的所有者。好的文档总是记得讨论一个函数期望应用到它的参数还是值的所有权规则。或者这么说,文档中频繁的错误是忘了提及,一个参数或者返回值的归属法则是什么。这是内存错误和泄露的一个原因。

所有权模型

所有权的两个共同模式是:

调用者所有权——调用者拥有自己的内存。出于共享的目的,它可能给被调用者传递一个指针,但是调用者保存归属权。当被调用者运行时,被调用者可以获取到东西,分配释放自己的内存,但不应该破坏调用者的内存。

被调用者分配和返回——被调用者分配一些内存然后把它返回给调用者。这通常发生在被调用者计算结果需要新的内存存储和表达。新的内存被传递给调用者,因此他们能看到结果,调用者必须接管内存的所有权。这是StringCopy()函数说明的模式。

堆内存总结

堆内存为程序员提供了更大的控制权——内存块可以申请任意大小,保留分配直到明确释放。堆内存可以被传递回调用者因为函数退出后并未自动释放,这可以用来建立链式结构比如链表和二叉树。堆内存的缺点在于程序必须明确堆内存的分配和释放。堆内存不按局部内存那样自动处理。

/* Given a C string, return a heap allocated copy of the string. Allocate a block in the heap of the appropriate size, copies the string into the block, and returns a pointer to the block. The caller takes over ownership of the block and is responsible for freeing it.*/char* StringCopy(const char* string) {char* newString;int len;len = strlen(string) + 1; // +1 to account for the '\0'newString = malloc(sizeof(char)*len); // elem-size * number-of-elementsassert(newString != NULL); // simplistic error check (a good habit)strcpy(newString, string); // copy the passed in string to the blockreturn(newString); // return a ptr to the block}
1 0
原创粉丝点击