当前位置: 首页 > 科技观察

C语言最大难点:编程的克星!

时间:2023-03-13 19:34:14 科技观察

本文将带您了解一些与内存相关的良好编码实践,以控制内存错误。内存错误是C和C++编程的祸根:它们很常见,二十多年来人们已经认识到它们的严重性但从未完全解决,它们会严重影响应用程序,并且很少有开发团队对其计划进行明确的管理。但好消息是,它们并不是很神秘。简介C和C++程序中的内存错误是非常有害的:它们很常见并且会产生严重的后果。来自计算机紧急响应团队(请参阅参考资料)和供应商的许多最严重的安全建议都是由简单的内存错误引起的。自70年代后期以来,C程序员就一直在讨论这种类型的错误,但它的影响在今天仍然很强烈。更糟糕的是,如果您按照我的想法来思考它,那么今天的许多C和C++程序员可能认为内存错误是无法控制的神秘疾病,只能纠正,不能预防。但事实并非如此,本文将使您在短时间内了解所有与内存相关的良好编码的精髓:正确内存管理的重要性内存错误的C和C++程序可能会导致各种问题。如果它们泄漏内存,它们会逐渐变慢并最终停止,如果它们覆盖内存,它们就会变得非常容易受到恶意用户的攻击。从1988年著名的莫里斯蠕虫攻击到最新的关于FlashPlayer和其他关键零售级程序的安全警报都与缓冲区溢出有关:“大多数计算机安全漏洞都是缓冲区溢出,”RodneyBates在2004年的道路上写道。在可以使用C或C++的地方,许多其他通用语言也得到广泛支持(如Java?、Ruby、Haskell、C#、Perl、Smalltalk等),各有各的拥趸和优势。然而,从计算的角度来看,每种编程语言相对于C或C++的主要优势都与内存管理的简便性密切相关。与内存相关的编程非常重要,但在实践中很难正确应用,以至于它支配了所有面向对象的编程语言、函数式编程语言、高级编程语言、声明式编程语言和其他一些语言。其他变量或理论。与少数其他类型的常见错误一样,内存错误是一种隐患:它们难以重现,并且通常在相应的源代码中找不到症状。例如,无论何时何地发生内存泄漏,应用程序可能看起来完全无法接受,而内存泄漏并不明显。因此,由于所有这些原因,C和C++编程中的内存问题需要特别注意。让我们看看如何解决这些问题,不管是哪种语言。记忆错误的类别首先,不要失去信心。有很多方法可以处理内存问题。让我们首先列出所有可能存在的实际问题:内存泄漏分配不当,包括释放内存和未初始化引用的大量增加悬垂指针数组越界这是所有类型。即使迁移到C++面向对象语言,这些类型也不会发生显着变化;C和C++中的内存管理和引用模型原则上是相同的,无论数据是简单类型还是C结构或C++类。下面的大部分内容都是“纯C”的,对C++的扩展大多留作练习。内存泄漏内存泄漏发生在资源被分配但从未被回收时。这是一个可能出错的模型(参见清单1):清单1.简单的潜在堆内存丢失和缓冲区覆盖)sprintf(p1,"Thef1erroroccurredbecauseof'%s'.,explanation);local_log(p1);}你看到问题了吗?除非local_log()异常响应free()释放的内存,否则每次调用f1都会泄漏100字节。当记忆棒逐渐分配数兆字节的内存时,一次泄漏微不足道,但在连续运行数小时后,即使是这么小的泄漏也会使应用程序瘫痪。在实际的C和C++编程中,这并不足以影响你使用malloc()或new,而本节开头的那句话提到“资源”不仅仅意味着“内存”,因为还有例如(请参见清单2)。FILE句柄可能与内存块不同,但必须给予同等重视:清单2.资源管理不善导致的潜在堆内存丢失filename,"r");fscanf(fp,"%d",&key);returnkey;}fopen的语义需要互补的fclose。当C标准没有指定没有fclose()时会发生什么,很可能是内存泄漏。其他资源(如信号量、网络句柄、数据库连接等)也值得考虑。内存错配错配并不难管理。这是一个示例(参见清单3):清单3.未初始化的指针这是参考片段:voidf2(intdatum){int*p2;/*Uh-oh!Noonehasinitializedp2.*/*p2=datum;…}关于此类错误的好消息是它们通常会产生严重的后果。在AIX下,分配给未初始化的指针通常会立即导致分段错误。好处是可以快速检测到任何此类错误;与需要数月才能确定且难以重现的错误相比,检测它们的成本要低得多。此错误类型有多种变体。free()比malloc()更频繁地释放内存(参见清单4):清单4.两个错误的内存释放这是参考片段:/*Allocateonce,freetwice.*/voidf3(){char*p;p=malloc(10);...free(p);...free(p);}/*Allocatezerotimes,freeonce.*/voidf4(){char*p;/*注意premainsuninitializedhere.*/free(p);这些错误通常也不太严重。尽管C标准没有定义这些情况下的特定行为,但典型的实现将忽略错误,或快速明确地标记错误。简而言之,这些都是安全的情况。悬挂指针悬挂指针比较棘手。当程序员在内存资源被释放后使用它时会出现悬空指针(参见清单5):清单5.悬空指针下面是引用片段:voidf8(){structx*xp;xp=(structx*)malloc(sizeof(structx));xp.q=13;...free(xp);.../*问题!不能保证xppoint所在的内存块没有被覆盖。*/returnxp.q;}传统的“调试”很难隔离悬挂指针。由于两个明显的原因,它们很难重现:即使影响提前释放内存范围的代码是本地化的,内存使用仍可能取决于应用程序中的其他执行位置,甚至(在极端情况下)在不同进程中。悬挂指针可能发生在以微妙方式使用内存的代码中。结果是即使内存释放后立即被覆盖,新指向的值与预期不同,也很难识别新值是错误的值。悬空指针对C或C++程序的行为是一个持续的威胁。ArrayBoundsViolationsArrayboundsviolations是危险的,也是内存错误管理的最后一个主要类别。回顾清单1;如果解释的长度超过80会怎样?回答:不可预测,但可能远非好事。特别是,C复制了一个不适合分配给它的100个字符的字符串。在任何常规实现中,“超出”的字符会覆盖内存中的其他数据。内存中数据分配的布局复杂且难以重现,因此不可能将任何症状追溯到源代码级别的特定错误。这些错误通常会造成数百万美元的损失。内存编程策略勤奋和纪律可以最大限度地减少这些错误的影响。下面,我们概述了您可以采取的一些具体步骤。我在各种组织中与它们打交道的经验是,内存错误至少持续减少了一个数量级。CodingstyleCodingstyle是最重要的,我从来没有看到任何其他作者强调这一点。影响资源(尤其是内存)的函数和方法需要明确地解释它们自己。下面是标题、注释或名称的一些示例(参见清单6)。清单6.用于识别资源的源代码示例下面是引用片段:/********...**注意任何函数调用protected_file_read()*assumesresponsibilityeventuallytofclose()its*returnvalue,UNLESSthatvalueisNULL.*********/FILE*protected_file_read(char*filename){FILE*fp;fp=fopen(filename,"r");if(fp){...}else{...}returnfp;}/********...**请注意,get_message的返回值指向一个*固定的内存位置。不要免费()它;记住*makecopyifit必须保留...********/char*get_message(){staticcharthis_buffer[400];...(void)sprintf(this_buffer,...);returnthis_buffer;}/*********...*虽然此函数使用堆内存,因此*暂时可能会扩展整体内存*占用空间,但它会在自身后正确清理。********/intf6(char*item1){my_classc1;intresult;...c1=newmy_class(item1);...result=c1.x;deletec1;returnresult;}/********...*注意f8()被记录为返回一个值*需要返回到堆中;asf7thinly*wrapsf8,任何调用f7()的代码必须*小心释放()返回值.********/int*f7(){int*p;p=f8(...);...returnp;}让这些格式元素成为您日常工作的一部分内存问题可以使用多种方法解决:专用库语言软件工具硬件检查器在投资上是考虑改进源代码的风格。它不需要昂贵或形式严格;与内存无关的段总是可以取消注释,但影响内存的定义当然需要明确注释。加几个简单的词,记忆结果更清晰,记忆编程提高。我没有做过对照实验来验证这种风格的效果。如果您的经历和我一样,您会发现不考虑资源影响的政策简直难以忍受。做起来很简单,但好处是压倒性的。检测检测是对编码标准的补充。两者都有其优点,但它们一起工作时特别好。精明的C或C++专业人员甚至可以浏览不熟悉的源代码,并以极低的成本检测内存问题。通过一些练习和适当的文本搜索,您可以快速验证平衡的*alloc()和free()或new和delete的源代码体。手动查看此类内容通常会产生清单7中的问题。清单7.棘手的内存泄漏以下是参考片段:staticchar*important_pointer=NULL;voidf9(){if(!important_pointer)important_pointer=malloc(IMPORTANT_SIZE);...if(condition)/*Ooops!Wejustlostthereferenceimportant_pointeralreadyheld.*/important_pointer=malloc(DIFFERENT_SIZE);...}如果条件为真,仅使用自动运行时工具无法检测到发生的内存泄漏。仔细的源分析可以从这些条件推断出合理的结论。我将重复我写的关于风格的内容:虽然许多已发布的内存问题描述都强调工具和语言,但对我来说最大的收获来自“软”以开发人员为中心的流程变化。您在样式和检测方面所做的任何改进都可以帮助您了解自动化工具产生的诊断结果。静态自动语法分析当然,人类并不是唯一可以阅读源代码的人。您还应该将静态语法分析作为开发过程的一部分。静态语法分析是lint、严格编译和一些商业产品所做的:扫描编译器接受的源文本和对象项,但这可能是错误的征兆。希望让您的代码保持无lint。尽管lint已经过时并且有些局限性,但许多不使用它(或其更高级的后代)的程序员会犯一个大错误。通常,您可以编写忽略lint的优质、专业质量的代码,但尝试这样做通常会导致重大错误。其中一些错误会影响记忆的正确性。与让客户首先发现内存错误的成本相比,即使为此类产品支付最昂贵的许可费也毫无价值。清洁源代码。现在,即使带有lint标记的编码可能会满足您的需求,但可能有一种更简单的方法可以满足lint并且更强大、更便携。最后两类记忆库修复与前三类有很大不同。前者轻巧;人们可以很容易地理解和实施它们。另一方面,内存库和工具的许可费用通常很高,对于一些开发人员来说,它们需要进一步完善和调整。有效使用库和工具的程序员是了解轻量级静态方法的程序员。可用的库和工具令人印象深刻:作为一个群体的质量很高。但即使是最优秀的程序员也会被那些忽视内存管理基本原则的非常任性的程序员搞得一团糟。据我所见,普通程序员只有在尝试孤立地使用内存库和工具时才会感到沮丧。由于这些原因,我们敦促C和C++程序员首先了解它们的来源,以便解决内存问题。完成后,考虑图书馆。使用多个库可以编写常规的C或C++代码,并有望改进内存管理。JonathanBartlett在他2004年的developerWorks评论专栏中描述了主要候选人,可在下面的参考资料部分找到。图书馆解决了太多不同的记忆问题,直接比较它们很困难;该领域的常见主题包括垃圾收集、智能指针和智能容器。一般来说,库可以自动进行更多的内存管理,这样程序员就可以少犯错误。我对记忆库的感觉很复杂。他们正在努力工作,但我看到他们在项目中的成功不如预期,尤其是在C方面。我还没有剖析这些令人失望的结果。例如,性能应该与相应的手动内存管理一样好,但这是一个灰色区域——尤其是当垃圾收集库处理它很慢时。从这种实践中得出的最明确的结论是,C++似乎比以C为中心的代码组更能接受智能指针。内存工具开发真正基于C的应用程序的开发团队需要运行时内存工具作为其开发策略的一部分。所介绍的技术是有价值的,不可或缺的。在您亲自尝试之前,您可能不知道记忆工具的质量和功能。本文主要讨论基于软件的记忆工具。还有硬件内存调试器;它们仅在非常特殊的情况下才会被考虑(主要是在使用不支持其他工具的专用主机时)。市场上的软件内存工具包括专有工具(如IBMRationalPurify和ElectricFence)和其他开源工具。其中许多与AIX和其他操作系统配合良好。所有内存中工具基本上都做同样的事情:构建特定版本的可执行文件(很像在编译时使用-g标志生成的调试版本),运行关联的应用程序,并研究工具自动生成的报告.考虑清单8中所示的程序。清单8.示例错误下面是引用片段:intmain(){charp[5];strcpy(p,"Hello,world.");puts(p);}这个程序可以是在许多环境中“运行”,它会编译、执行并将“Hello,world.n”打印到屏幕上。使用内存工具运行相同的应用程序会在第4行生成数组边界违规报告。在了解软件错误方面(十四个字符被复制到只能容纳五个的空间),这种方法比查找症状要便宜得多客户的错误。这要归功于内存中的工具。结论作为一名成熟的C或C++程序员,您意识到内存问题值得特别注意。通过一些计划和实践,您可以找到控制记忆错误的方法。了解内存使用的正确模式,快速发现可能发生的错误,并使本文中介绍的技术成为您日常工作的一部分。您可以从一开始就消除应用程序中的症状,否则可能需要数天或数周才能进行调试。