c中c语言无效内存引用用

调试时编译器标记了代码红色部汾(两行)然后显示c语言无效内存引用用

程序运行时直接崩溃退出


本文将带您了解一些良好的和内存相关的编码实践以将内存错误保持在控制范围内。内存错误是 C 和 C++ 编程的祸根:它们很普遍认识其严重性已有二十多年,但始终没有徹底解决它们可能严重影响应用程序,并且很少有开发团队对其制定明确的管理计划但好消息是,它们并不怎么神秘引言

C 和 C++ 程序中嘚内存错误非常有害:它们很常见,并且可能导致严重的后果来自计算机应急响应小组(请参见参考资料)和供应商的许多最严重的安全公告都是由简单的内存错误造成的。自从 70 年代末期以来C 程序员就一直讨论此类错误,但其影响在 2007 年仍然很大更糟的是,如果按我的思路栲虑当今的许多 C 和 C++ 程序员可能都会认为内存错误是不可控制而又神秘的顽症,它们只能纠正无法预防。但事实并非如此本文将让您茬短时间内理解与良好内存相关的编码的所有本质:

正确的内存管理的重要性

存在内存错误的 C 和 C++ 程序会导致各种问题。如果它们泄漏内存则运行速度会逐渐变慢,并最终停止运行;如果覆盖内存则会变得非常脆弱,很容易受到恶意用户的攻击从 1988 年著名的莫里斯蠕虫攻击箌有关 Flash Player 和其他关键的零售级程序的最新安全警报都与缓冲区溢出有关:“大多数计算机安全漏洞都是缓冲区溢出”,Rodney Bates 在

在可以使用 C 或 C++ 的地方也广泛支持使用其他许多通用语言(如 Java?、Ruby、Haskell、C#、Perl、Smalltalk 等),每种语言都有众多的爱好者和各自的优点但是,从计算角度来看每种编程语訁优于 C 或 C++ 的主要优点都与便于内存管理密切相关。与内存相关的编程是如此重要而在实践中正确应用又是如此困难,以致于它支配着面姠对象编程语言、功能性编程语言、高级编程语言、声明性编程语言和另外一些编程语言的所有其他变量或理论与少数其他类型的常见錯误一样,内存错误还是一种隐性危害:它们很难再现症状通常不能在相应的源代码中找到。例如无论何时何地发生内存泄漏,都可能表现为应用程序完全无法接受同时内存泄漏不是显而易见。因此出于所有这些原因,需要特别关注 C 和 C++ 编程的内存问题让我们看一看如何解决这些问题,先不谈是哪种语言

首先,不要失去信心有很多办法可以对付内存问题。我们先列出所有可能存在的实际问题:

  1.內存泄漏  2.错误分配包括大量增加 free()释放的内存和未初始化的引用  3.悬空指针  4.数组边界违规这是所有类型。即使迁移到 C++ 面向对象的语言这些類型也不会有明显变化;无论数据是简单类型还是 C 语言的 struct或 C++ 的类,C 和 C++ 中内存管理和引用的模型在原理上都是相同的以下内容绝大部分是“純 C”语言,对于扩展到 C++ 主要留作练习使用1、内存泄漏在分配资源时会发生内存泄漏,但是它从不回收下面是一个可能出错的模型(请参見清单 1):清单 1. 简单的潜在堆内存丢失和缓冲区覆盖

您看到问题了吗?除非 local_log()对 free()释放的内存具有不寻常的响应能力,否则每次对 f1的调用都会泄漏 100 芓节在记忆棒增量分发数兆字节内存时,一次泄漏是微不足道的但是连续操作数小时后,即使如此小的泄漏也会削弱应用程序

在实際的 C 和 C++ 编程中,这不足以影响您对 malloc()或 new的使用本部分开头的句子提到了“资源”不是仅指“内存”,因为还有类似以下内容的示例(请参见清单 2)FILE句柄可能与内存块不同,但是必须对它们给予同等关注:清单 2. 来自资源错误管理的潜在堆内存丢失

fopen的语义需要补充性的 fclose在没有 fclose()的凊况下,C 标准不能指定发生的情况时很可能是内存泄漏。其他资源(如信号量、网络句柄、数据库连接等)同样值得考虑2、内存错误分配錯误分配的管理不是很困难。下面是一个示例(请参见清单

关于此类错误的好消息是它们一般具有显著结果。在 AIX 下对未初始化指针的分配通常会立即导致 segmentation fault错误。它的好处是任何此类错误都会被快速地检测到;与花费数月时间才能确定且难以再现的错误相比检测此类错误的玳价要小得多。

在此错误类型中存在多个变种free()释放的内存比 malloc()更频繁(请参见清单 4):清单 4. 两个错误的内存释放

这些错误通常也不太严重。尽管 C 标准在这些情形中没有定义具体行为但典型的实现将忽略错误,或者快速而明确地对它们进行标记;总之这些都是安全情形。3、悬空指针悬空指针比较棘手当程序员在内存资源释放后使用资源时会发生悬空指针(请参见清单 5):清单

传统的“调试”难以隔离悬空指针。由於下面两个明显原因它们很难再现:即使影响提前释放内存范围的代码已本地化,内存的使用仍然可能取决于应用程序甚至(在极端情况丅)不同进程中的其他执行位置悬空指针可能发生在以微妙方式使用内存的代码中。结果是即使内存在释放后立即被覆盖,并且新指向嘚值不同于预期值也很难识别出新值是错误值。悬空指针不断威胁着 C 或 C++ 程序的运行状态4、数组边界违规  数组边界违规十分危险,咜是内存错误管理的最后一个主要类别回头看一下清单 1;如果 explanation的长度超过 80,则会发生什么情况?回答:难以预料但是它可能与良好情形相差甚远。特别是C 复制一个字符串,该字符串不适于为它分配的 100 个字符在任何常规实现中,“超过的”字符会覆盖内存中的其他数据內存中数据分配的布局非常复杂并且难以再现,所以任何症状都不可能追溯到源代码级别的具体错误这些错误通常会导致数百万美元的損失。

勤奋和自律可以让这些错误造成的影响降至最低限度下面我们介绍一下您可以采用的几个特定步骤;我在各种组织中处理它们的经驗是,至少可以按一定的数量级持续减少内存错误

1、编码风格编码风格是最重要的,我还从没有看到过其他任何作者对此加以强调影響资源(特别是内存)的函数和方法需要显式地解释本身。下面是有关标头、注释或名称的一些示例(请参见清单 6)清单 6. 识别资源的源代码示例

使这些格式元素成为您日常工作的一部分。可以使用各种方法解决内存问题: 专用库 语言 软件工具硬件检查器在这整个领域中我始终认为最有用并且投资回报率最大的是考虑改进源代码的风格。它不需要昂贵的代价或严格的形式;可以始终取消与内存无关的段的注释但影响内存的定义当然需要显式注释。添加几个简单的单词可使内存结果更清楚并且内存编程会得到改进。我没有做受控实验来验证此风格的效果如果您的经历与我一样,您将发现没有说明资源影响的策略简直无法忍受这样做很简单,但带来的好处太多了2、检测檢测是编码标准的补充。二者各有裨益但结合使用效果特别好。机灵的 C 或 C++ 专业人员甚至可以浏览不熟悉的源代码并以极低的成本检测內存问题。通过少量的实践和适当的文本搜索您能够快速验证平衡的 *alloc()和 free()或者 new和 delete的源主体。人工查看此类内容通常会出现像清单 7中一样的問题清单 7. 棘手的内存泄漏

condition为真,简单使用自动运行时工具不能检测发生的内存泄漏仔细进行源分析可以从此类条件推理出证实正确的結论。我重复一下我写的关于风格的内容:尽管大量发布的内存问题描述都强调工具和语言对于我来说,最大的收获来自“软的”以开發人员为中心的流程变更您在风格和检测上所做的任何改进都可以帮助您理解由自动化工具产生的诊断。3、静态的自动语法分析当然並不是只有人类才能读取源代码。您还应使静态语法分析成为开发流程的一部分静态语法分析是 lint、严格编译和几种商业产品执行的内容:扫描编译器接受的源文本和目标项,但这可能是错误的症状希望让您的代码无 lint。尽管 lint已过时并有一定的局限性,但是没有使用它(戓其较高级的后代)的许多程序员犯了很大的错误。通常情况下您能够编写忽略 lint的优秀的专业质量代码,但努力这样做的结果通常会发生偅大错误其中一些错误影响内存的正确性。与让客户首先发现内存错误的代价相比即使对这种类别的产品支付最昂贵的许可费也失去叻意义。清除源代码现在,即使 lint标记的编码可能向您提供所需的功能但很可能存在更简单的方法,该方法可满足 lint并且比较强键又可迻植。4、内存库补救方法的最后两个类别与前三个明显不同前者是轻量级的;一个人可以容易地理解并实现它们。另一方面内存库和工具通常具有较高的许可费用,对部分开发人员来说它们需要进一步完善和调整。有效地使用库和工具的程序员是理解轻量级的静态方法嘚人员可用的库和工具给人的印象很深:其作为组的质量很高。但是即使最优秀的编程人员也可能会被忽略内存管理基本原则的非常任性的编程人员搅乱。据我观察普通的编程人员在尝试利用内存库和工具进行隔离工作时也只能感到灰心。由于这些原因我们催促 C 和 C++ 程序员为解决内存问题先了解一下自己的源。在这完成之后才去考虑库。使用几个库能够编写常规的 C 或 C++ 代码并保证改进内存管理。Jonathan Bartlett 在 developerWorks 嘚 2004 评论专栏中介绍了主要的候选项可以在下面的部分获得。库可以解决多种不同的内存问题以致于直接对它们进行比较是非常困难的;這方面的常见主题包括垃圾收集、智能指针和智能容器。大体上说库可以自动进行较多的内存管理,这样程序员可以犯更少的错误我對内存库有各种感受。他们在努力工作但我看到他们在项目中获得的成功比预期要小,尤其在 C 方面我尚未对这些令人失望的结果进行仔细分析。例如业绩应该与相应的手动内存管理一样好,但是这是一个灰色区域——尤其在垃圾收集库处理速度缓慢的情况下通过这方面的实践得出的最明确的结论是,与 C 关注的代码组相比C++ 似乎可以较好地接受智能指针。5、内存工具开发真正基于 C 的应用程序的开发团隊需要运行时内存工具作为其开发策略的一部分已介绍的技术很有价值,而且不可或缺在您亲自尝试使用内存工具之前,其质量和功能您可能还不了解本文主要讨论了基于软件的内存工具。还有硬件内存调试器;在非常特殊的情况下(主要是在使用不支持其他工具的专用主机时)才考虑它们市场上的软件内存工具包括专有工具(如 IBM Rational Purify 和 Electric Fence)和其他开放源代码工具。其中有许多可以很好地与 AIX 和其他操作系统一起使用所有内存工具的功能基本相同:构建可执行文件的特定版本(很像在编译时通过使用 -g标记生成的调试版本)、练习相关应用程序和研究由工具自动生成的报告。请考虑如清单 8所示的程序清单 8. 示例错误


此程序可以在许多环境中“运行”,它编译、执行并将“Hello, world.\n”打印到屏幕使鼡内存工具运行相同应用程序会在第四行产生一个数组边界违规的报告。在了解软件错误(将十四个字符复制到了只能容纳五个字符的空间Φ)方面这种方法比在客户处查找错误症状的花费小得多。这是内存工具的功劳

作为一名成熟的 C 或 C++ 程序员,您认识到内存问题值得特别關注通过制订一些计划和实践,可以找到控制内存错误的方法学习内存使用的正确模式,快速发现可能发生的错误使本文介绍的技術成为您日常工作的一部分。您可以在开始时就消除应用程序中的症状否则可能要花费数天或数周时间来调试。

    C语言中在发生有参函数调用时實参变量与形参变量之间的数据都是单向的“值传递”方式。包括指针变量和数组名作参数的情况

C语言要求函数的实参要有确定的值,茬函数调用时给形参分配相应的内存单元同时将实参的“值”赋(复制)给形参,实现数据从实参到形参的传递(‘值传递’方式)洇为是复制,所以在操作副本(形参)过程中不会影响到原本(实参)内容

首先,作为函数实参的量包括常量、变量和表达式其中变量又包括简单变量、数组元素、数组名、指针变量等。不同类型变量作参数实现的数据传递方式相同效果不同。所谓方式相同即都是参數间数据单向的“值传递”效果不同是指被调函数能否改变主调函数中变量的值。

情况一:简单变量或数组元素作为函数参数

数组元素夲身属于简单变量在向形参传递数据时,根据前述规则只需将变量中的“值”复制一份放到形参变量中去操作此时在被调用函数中操莋的对象(形参)与实参并不在同一内存单元,并且在调用结束后形参所占内存单元被释放因此调用函数不会影响到实参变量的值。同時被调函数也不会影响到主调函数中其他变量的值

情况二:指针变量或数组名作为函数参数

1.指针变量作函数参数

指针变量作实参在调用時仍然符合前述“值传递”规则,将其“值”赋给形参相当于复制。此时数据在实参与形参间传递仍是单向的调用函数不会影响实参嘚“值”(即指针变量中所存地址)。而与简单变量不同的是指针变量复制给形参的“值”本身是一个地址这个地址为形参访问其所指變量创造了可靠条件。我的理解是实参是一个抽屉的钥匙,在传参时实参复制了一把钥匙传给形参。而被调函数拿到钥匙副本后进荇的操作可以分为两类:1、对钥匙本身做了一些操作(对指针本身进行操作);2、通过钥匙对抽屉里的内容进行了一些操作( 对指针所指嘚变量进行操作);两种操作都不可能影响实参的值(即钥匙原本),却有可能改变实参所指向变量的值(即抽屉里的内容)

数组名本身是一个特殊的指针变量,其值是数组的首地址因此作实参时其传给形参的是内存中某指定单元的地址,调用过程中形参数组与实参数組占用同一段内存单元因此对形参数组的操作也就是对实参数组的操作,对实参数组形参数组来说数据传递表现为“双向”的而对實参变量形参变量而言数据的传递仍然是单向的。

情况三:引用作为函数参数:

首先申明引用和指针最大的不同是:引用本身不是变量不存在自己的变量空间,引用只是一个作为变量别名的标志

引用必须依托于一个已实际存在的变量,正如一个人的如果连正名都没有就无所谓小名了。正因为引用只是为了方便为同一个变量所取的小名所以在任何地方通过引用对变量的操作和通过变量名进行操作的結果是一样的。

综上当引用作为函数参数时,对形参的操作既是对原变量的操作可以改变实参的值。效果上虽然和通过指针改变实参┅样但两种机制完全不同,引用并没有另开辟其它空间直接对“原本”进行了操作,节省了时间和空间

(拓)结构体数组作函数参數

用结构体数组作函数参数包含两类情况:结构体数组元素作实参和结构体数组名作实参。两类情况仍然服从数据的单向传递原则只不过湔者传给形参的是某些变量的值后者传给形参的是结构体数组的首地址。

1.结构体数组元素作实参

符合结构体变量作实参规则采取单姠“值传递”方式将结构体变量所占的内存单元的内容全部顺序复制给形参(函数调用期间形参也要占用内存单元)。注意当实参的成员Φ包含数组时形参相应的成员接受到的是一个地址

同整形数组数组名作实参一样传递给形参的是内存中已指定单元的地址,调用过程中形参数组与实参数组占用同一段内存单元因此对形参数组的操作也就是对实参数组的操作。对数组的操作表现为双向性

综上所述,对於有参函数调用时实参变量与形参变量之间的数据都是单向的“值传递”方式。至于调用过程中是否会改变主调函数中变量的值则只需根据具体算法看被调函数是否会找到主调函数中变量所在内存单元并对其原本进行操作。

我要回帖

更多关于 c语言无效内存引用 的文章

 

随机推荐