Profil de Libin绿色家园PhotosBlogListes Outils Aide

Blog


27 novembre

C++资源之不完全导引(完整版)

这么牛的文章,不转载不行!!!
 
来源:www.csdn.net

撰文:曾毅、陶文

声明:本文2004年5月首发于《CSDN开发高手》,版权归该杂志与《程序员》杂志社所有。

--------------------------------------------------------------------------------

  1,前言

  无数次听到"我要开始学习C++!"的呐喊,无数次听到"C++太复杂了,我真的
学不会"的无奈。Stan Lippman先生曾在《C++ Primer》一书中指出"C++是最为难
学的高级程序设计语言之一",人们常将"之一"去掉以表达自己对C++的敬畏。诚
然,C++程序设计语言对于学习者的确有很多难以逾越的鸿沟,体系结构的庞大,应
接不暇并不断扩充的特性……除此之外,参考资料之多与冗杂使它的学习者望而却
步,欲求深入者苦不堪言。希望这一份不完全导引能够成为您C++学习之路上的引路
灯。

  撰写本文的初衷并不打算带领大家体验古老的C++历史,如果你想了解C++的历
史与其前期发展中诸多技术的演变,你应当去参考Bjarne的《The Design and Evo
lution of C++》。当然也不打算给大家一个无所不包的宝典(并非不想:其一是因
水平有限,其二无奈C++之博大精深),所给出的仅仅是一些我们认为对于想学习C
++的广大读者来说最重要并且触手可及的开发与学习资源。

  本文介绍并分析了一些编译器,开发环境,库,少量的书籍以及参考网站,并
且尽可能尝试着给出一个利用这些资源的导引,望对如同我们一样的初学者能够有
所裨益。

------------------------------------------------------------------------
--------

  2,编译器

  在C++之外的任何语言中,编译器都从来没有受到过如此之重视。因为C++是一
门相当复杂的语言,所以编译器也难于构建。直到最近我们才开始能够使用上完全
符合C++标准的编译器(哦,你可能会责怪那些编译器厂商不能尽早的提供符合标准
的编译器,这只能怪他们各自维系着自身的一套别人不愿接受的标准)。什么?你
说这无关紧要?哦,不,你所需要的是和标准化C++高度兼容的编译环境。长远来看
,只有这样的编译器对C++开发人员来说才是最有意义的工具,尤其是对于程序设计
语言的学习者。一至性让代码具备可移植性,并让一门语言及其库的应用更为广泛
。嗯,是的,我们这里只打算介绍一些公认的优秀编译器。

  2.1 Borland C++

  这个是Borland C++ Builder和Borland C++ Builder X这两种开发环境的后台
编译器。(哦,我之所以将之分为两种开发环境你应当能明白为什么,正如Delphi
7到Delphi8的转变,是革命性的两代。)Borland C++由老牌开发工具厂商Borland
倾力打造。该公司的编译器素以速度快,空间效率高著称,Borland C++ 系列编译
器秉承了这个传统,属于非常优质的编译器。标准化方面早在5.5版本的编译器中对
标准化C++的兼容就达到了92.73%。目前最新版本是Borland C++ Builder X中的6.
0版本,官方称100%符合ANSI/ISO的C++标准以及C99标准。嗯…这正是我前面所指的
"完全符合C++标准的编译器"。

  2.2 Visual C++

  这个正是我们熟知的Visual Studio 和 Visual Studio.net 2002, 2003以及2
005 Whidbey中带的C++编译器。由Microsoft公司研制。在Visual Studio 6.0中,
因为编译器有太多地方不能与后来出现的C++标准相吻合而饱受批评(想想你在使用
STL的时候编译时报出的那些令人厌恶的error和warning吧)。VC++6.0对标准化C+
+的兼容只有83.43%。但是随着C++编译器设计大师Stanley Lippman以及诸多C++社
群达人的加盟,在Visual Studio.NET 2003中,Visual C++编译器已经成为一个非
常成熟可靠的C++编译器了。Dr.Dobb's Journal的评测显示Visual C++7.1对标准C
++的兼容性高达98.22%,一度成为CBX之前兼容性最好的编译器。结合强大的Visua
l Studio.NET开发环境,是一个非常不错的选择。至于Whidbey时代的Visual C++,
似乎微软所最关注的是C++/CLI……我们不想评论微软下一代的C++编译器对标准化
兼容如何,但他确实越来越适合.NET (其实你和我的感觉可能是一样的,微软不应
当把标准C++这块肥肉丢给Borland,然而微软可能并不这样认为)。

  2.3 GNU C++

  著名的开源C++编译器。是类Unix操作系统下编写C++程序的首选。特点是有非
常好的移植性,你可以在非常广泛的平台上使用它,同时也是编写跨平台,嵌入式
程序很好的选择。另外在符合标准这个方面一直都非常好,GCC3.3大概能够达到96
.15%。但是由于其跨平台的特性,在代码尺寸速度等优化上略微差一点。

  基于GNU C++的编译器有很多,比如:

  (1) Mingw

  http://www.mingw.org/

  GCC的一个Windows的移植版本(Dev-C++的后台)

  (2) Cygwin

  http://sources.redhat.com/cygwin/

  GCC的另外一个Windows移植版本是Cygwin的一部分,Cygwin是Windows下的一个
Unix仿真环境。严格的说是模拟GNU的环境,这也就是"Gnu's Not Unix"要表达的意
思,噢,扯远了,这并不是我们在这里关心的实质内容。

  (3) Djgpp

  http://www.delorie.com/djgpp/

  这是GCC的DOS移植版本。

  (4) RSXNT

  http://www.mathematik.uni-bielefeld.de/~rainer/

  这是GCC的DOS和Windows移植版本。

  (5) Intel C++

  著名CPU制造厂商Intel出品的编译器,Special Design for Intel x86!对于
Intel x86结构的CPU经过特别的优化。在有些应用情况下,特别是数值计算等高性
能应用,仅仅采用Intel的编译器编译就能大幅度的提高性能。

  (6) Digital Mars C++

  网络上提供免费下载,Zortech/Symantec C++的继承者,其前身在当年惨烈的
C++四国战中也是主角之一。

------------------------------------------------------------------------
--------

  3,开发环境

  开发环境对于程序员的作用不言而喻。选择自己朝夕相处的环境也不是容易的
事情,特别是在IDE如此丰富的情况下。下面就是我们推荐的一些常见的C++开发环
境,并没有包括一些小型的,罕见的IDE。其中任何一款都是功能丰富,可以用作日
常开发使用的。对于不同层面的开发者,请参见内文关于适用对象的描述。

  3.1 Visual Studio 6.0

  这个虽然是Microsoft公司的老版本的开发环境,但是鉴于其后继版本Visual
Studio.NET的庞大身躯,以及初学者并不那么高的功能要求,所以推荐这个开发环
境给C++的初学者,供其学习C++的最基本的部分,比如C的那部分子集,当然你别指
望他能够支持最新的C99标准。在日常的开发中,仍然有很多公司使用这个经典稳定
的环境,比如笔者就看曾亲见有些公司将其编译器替换为GCC做手机开发之用。

  3.2 Visual Studio.NET 2003

  作为Microsoft公司官方正式发布的最新版本开发环境,其中有太多激动人心的
功能。结合其最新的C++编译器。对于机器配置比较好的开发人员来说,使用这个开
发环境将能满足其大部分的要求。这里不打算单独说Visual Studio Whidbey,虽然
Visual Studio .NET 2005 - Whidbey社区预览版已经推出,但暂不是很稳定,读者
可以亲身去体验。

  3.3 Borland C++ Builder 6

  这个并不是Borland的C++开发环境的最新版本。选择它的原因是它不是用Java
写的IDE,速度比较快。它有一个很完善的GUI窗体设计器,和Delphi共用一个VCL。
由于这些特点,比较适合初学者上手。但是由于其GUI的中心位置,可能不利于对于
C++语言的学习。而且其为了支持VCL这个Object Pascal写的库也对C++进行了一些
私有的扩充。使得人们有一个不得不接受的事实:"Borland C++ Builder 6的高手
几乎都是Delphi高手"。

  3.4 Borland C++ Builder X

  正如前文所述,虽然版本号上和前面那个IDE非常相象,但是其实它们是完全不
同的两个集成开发环境。C++Builder更多的是一个和Delphi同步的C++版本的开发环
境,C++BuilderX则是完全从C++的角度思考得出的一个功能丰富的IDE。其最大的特
点是跨平台,跨编译器,多种Framework的集成,并且有一个WxWindows为基础的GU
I设计器。尤其是采用了纯C++来重写了整个Framework,摒弃了以前令人无奈的版本
。对于C++的开发来说,从编译器,到库,到功能集成都是非常理想的。可以预见,
Borland C++ Builder X 2.0很值得C++爱好者期待。唯一令人难堪之处是作为一个
C++的开发工具,其IDE是用Java写的,在配置不够理想的机器上请慎重考虑再安装

  3.5 Emacs + GCC

  前面讲的大部分是Windows环境下的集成开发环境。Linux上的开发者更倾向于
使用Emacs来编辑C++的文件,用Makefile来命令GCC做编译。虽然看上去比较松散,
但是这些东西综合起来还是一个开0发环境。如果你能够娴熟的使用这样的环境写程
序,你的水平应该足够指导我们来写这篇陋文了。

  3.6 Dev C++

  GCC是一个很好的编译器。在Windows上的C++编译器一直和标准有着一段距离的
时候,GCC就是一个让Windows下开发者流口水的编译器。Dev-C++就是能够让GCC跑
在Windows下的工具,作为集成开发环境,还提供了同专业IDE相媲美的语法高亮,
代码提示,调试等功能。由于使用Delphi开发,占用内存少,速度很快,比较适合
轻量级的学习和使用。

  3.7 Eclipse + CDT

  Eclipse可是近来大名鼎鼎的开发工具。最新一期的Jolt大奖就颁给了这个杰出
的神物。说其神奇是因为,它本身是用Java写的,但是拥有比一般Java写的程序快
得多的速度。而且因为其基于插件组装一切的原则,使得能够有CDT这样的插件把E
clipse变成一个C/C++的开发环境。如果你一直用Eclipse写Java的程序,不妨用它
体验一下C++开发的乐趣。

  3.8 codeblocks

  可是目前势头旺的开发工具。它是开源项目,主要用于C/C++的编译和调试,几乎可以配置全部的c/c++编译器,而且你可以随时自己进行修改,其本身的维护速度也相当的快,几乎每天都可以得到更新的版本。稳定版本可以去http://www.codeblocks.org下载,最新版本的更新也可以在nightly build(http://forums.codeblocks.org/index.php?board=20.0)得到。

------------------------------------------------------------------------
--------

  4,工具

  C++的辅助工具繁多,我们分门别类的为大家作介绍:

  4.1 文档类

  (1) Doxygen

  参考站点:http://www.doxygen.org

  Doxygen是一种适合C风格语言(如C++、C、IDL、Java甚至包括C#和PHP)的、
开放源码的、基于命令行的文档产生器。

  (2) C++2HTML

  参考站点:http://www.bedaux.net/cpp2html/

  把C++代码变成语法高亮的HTML

  (3) CodeColorizer

  参考站点:http://www.chami.com/colorizer/

  它能把好几种语言的源代码着色为HTML

  (4) Doc-O-Matic

  参考站点:http://www.doc-o-matic.com/

  Doc-O_Matic为你的C/C++,C++.net,Delphi/Pascal, VB.NET,C#和Java程序
或者组件产生准确的文档。Doc-O-Matic使用源代码中的符号和注释以及外部的文档
文件创建与流行的文档样式一致的文档。

  (5) DocVizor

  参考站点:http://www.ucancode.net/Products/DocBuilder/Features.htm

  DocVizor满足了面向对象软件开发者的基本要求——它让我们能够看到C++工程
中的类层次结构。DocVizor快速地产生完整可供打印的类层次结构图,包括从第三
方库中来的那些类,除此之外DocVizor还能从类信息中产生HTML文件。

  (6) SourcePublisher C++

  参考站点:http://www.scitools.com/sourcepublisher_c.html

  给源代码产生提供快速直观的HTML报表,包括代码,类层次结构,调用和被调
用树,包含和被包含树。支持多种操作系统。

  (7) Understand

  参考站点:http://www.scitools.com/ucpp.html

  分析任何规模的C或者C++工程,帮助我们更好的理解以及编写文档。

  4.2 代码类

  (1) CC-Rider

  参考站点:http://www.cc-rider.com

  CC-Rider是用于C/C++程序强大的代码可视化工具,通过交互式浏览、编辑及自
动文件来促进程序的维持和发展。

  (2) CodeInspect

  参考站点:http://www.yokasoft.com/

  一种新的C/C++代码分析工具。它检查我们的源代码找出非标准的,可能的,以
及普通的错误代码。

  (3) CodeWizard

  参考站点:http://www.parasoft.com

  先进的C/C++源代码分析工具,使用超过500个编码规范自动化地标明危险的,
但是编译器不能检查到的代码结构。

  (4) C++ Validation Test Suites

  参考站点:http://www.plumhall.com/suites.html

  一组用于测试编译器和库对于标准吻合程度的代码库。

  (5) CppRefactory

  参考站点:http://cpptool.sourceforge.net/

  CPPRefactory是一个使得开发者能够重构他们的C++代码的程序。目的是使得C
++代码的重构能够尽可能的有效率和简单。

  (6) Lzz

  参考站点:http://www.lazycplusplus.com/

  Lzz是一个自动化许多C++编程中的体力活的工具。它能够节省我们许多事件并
且使得编码更加有乐趣。给出一系列的声明,Lzz会给我们创建头文件和源文件。

  (7) QA C++ Generation 2000

  参考站点:http://www.programmingresearch.com/solutions/qacpp.htm

  它关注面向对象的C++源代码,对有关于设计,效率,可靠性,可维护性的部分
提出警告信息。

  (8) s-mail project - Java to C++DOL

  参考站点:http://sadlocha.strefa.pl/s-mail/ja2dol.html

  把Java源代码翻译为相应的C++源代码的命令行工具。

  (9) SNIP from Cleanscape Software International

  参考站点:http://www.cleanscape.net/stdprod/snip/index.html

  一个填平编码和设计之间沟壑的易于使用的C++开发工具,节省大量编辑和调试
的事件,它还使得开发者能够指定设计模式作为对象模型,自动从对象模型中产生
C++的类。

  (10) SourceStyler C++

  参考站点:http://www.ochresoftware.com/

  对C/C++源代码提供完整的格式化和排版控制的工具。提供多于75个的格式化选
项以及完全支持ANSI C++。

  4.3 编译类

  (1) Compilercache

  参考站点:http://www.erikyyy.de/compilercache/

  Compilercache是一个对你的C和C++编译器的封装脚本。每次我们进行编译,封
装脚本,把编译的结果放入缓存,一旦编译相同的东西,结果将从缓存中取出而不
是再次编译。

  (2) Ccache

  参考站点:http://ccache.samba.org/

  Ccache是一个编译器缓存。它使用起来就像C/C++编译器的缓存预处理器,编译
速度通常能提高普通编译过程的5~10倍。

  (3) Cmm (C++ with MultiMethods)

  参考站点:http://www.op59.net/cmm/cmm-0.28/users.html

  这是一种C++语言的扩展。读入Cmm源代码输出C++的源代码,功能是对C++语言
添加了对multimethod的支持。

  (4) The Frost Project

  参考站点:http://frost.flewid.de/

  Forst使得你能够在C++程序中像原生的C++特性一样使用multimethod以及虚函
数参数。它是一个编译器的外壳。

  4.4 测试和调试类

  (1) CPPUnit

  CppUnit 是个基于 LGPL 的开源项目,最初版本移植自 JUnit,是一个非常优
秀的开源测试框架。CppUnit 和 JUnit 一样主要思想来源于极限编程。主要功能就
是对单元测试进行管理,并可进行自动化测试。

  (2) C++Test

  参考站点:http://www.parasoft.com/

  C++ Test是一个单元测试工具,它自动化了C和C++类,函数或者组件的测试。


  (3) Cantata++

  参考站点:http://www.iplbath.com/products/tools/pt400.shtml

  设计的目的是为了满足在合理的经济开销下使用这个工具可以让开发工程师开
展单元测试和集成测试的需求.

  (4) Purify

  参考站点:http://www-900.ibm.com/cn/software/rational/products/purif
yplus/index.shtml

  IBM Rational PurifyPlus是一套完整的运行时分析工具,旨在提高应用程序的
可靠性和性能。PurifyPlus将内存错误和泄漏检测、应用程序性能描述、代码覆盖
分析等功能组合在一个单一、完整的工具包中。

  (5) BoundsChecker

  BoundsChecker是一个C++运行时错误检测和调试工具。它通过在Visual Studi
o内自动化调试过程加速开发并且缩短上市的周期。BoundsChecker提供清楚,详细
的程序错误分析,许多是对C++独有的并且在static,stack和heap内存中检测和诊
断错误,以及发现内存和资源的泄漏。  (6) Insure++

  参考站点:http://www.parasoft.com/

  一个自动化的运行时程序测试工具,检查难以察觉的错误,如内存覆盖,内存泄
漏,内存分配错误,变量初始化错误,变量定义冲突,指针错误,库错误,逻辑错
误和算法错误等。

  (7) GlowCode

  参考站点:http://www.glowcode.com/

  GlowCode包括内存泄漏检查,code profiler,函数调用跟踪等功能。给C++开
发者提供完整的错误诊断,和运行时性能分析工具包。

  (8) Stack Spy

  参考站点:http://www.imperioustech.com/

  它能捕捉stack corruption, stack over run, stack overflow等有关栈的错
误。

------------------------------------------------------------------------
--------

  5,库

  在C++中,库的地位是非常高的。C++之父 Bjarne Stroustrup先生多次表示了
设计库来扩充功能要好过设计更多的语法的言论。现实中,C++的库门类繁多,解决
的问题也是极其广泛,库从轻量级到重量级的都有。不少都是让人眼界大开,亦或
是望而生叹的思维杰作。由于库的数量非常庞大,而且限于笔者水平,其中很多并
不了解。所以文中所提的一些库都是比较著名的大型库。

  5.1 标准库

  标准库中提供了C++程序的基本设施。虽然C++标准库随着C++标准折腾了许多年
,直到标准的出台才正式定型,但是在标准库的实现上却很令人欣慰得看到多种实
现,并且已被实践证明为有工业级别强度的佳作。

  (1) Dinkumware C++ Library

  参考站点:http://www.dinkumware.com/

  P.J. Plauger编写的高品质的标准库。P.J. Plauger博士是Dr. Dobb's程序设
计杰出奖的获得者。其编写的库长期被Microsoft采用,并且最近Borland也取得了
其OEM的license,在其C/C++的产品中采用Dinkumware的库。

  (2) RogueWave Standard C++ Library

  参考站点:http://www.roguewave.com/

  这个库在Borland C++ Builder的早期版本中曾经被采用,后来被其他的库给替
换了。笔者不推荐使用。

  (3) SGI STL

  参考站点:http://www.roguewave.com/

  SGI公司的C++标准模版库。

  (4) STLport

  参考站点:http://www.stlport.org/

  SGI STL库的跨平台可移植版本。

  5.2 "准"标准库 - Boost

  参考站点:http://www.boost.org

  国内镜像:http://www.c-view.org/tech/lib/boost/index.htm

  Boost库是一个经过千锤百炼、可移植、提供源代码的C++库,作为标准库的后
备,是C++标准化进程的发动机之一。 Boost库由C++标准委员会库工作组成员发起
,在C++社区中影响甚大,其成员已近2000人。 Boost库为我们带来了最新、最酷、
最实用的技术,是不折不扣的"准"标准库。

  Boost中比较有名气的有这么几个库:

  Regex

  正则表达式库

  Spirit

  LL parser framework,用C++代码直接表达EBNF

  Graph

  图组件和算法

  Lambda

  在调用的地方定义短小匿名的函数对象,很实用的functional功能

  concept check

  检查泛型编程中的concept

 

  Mpl

  用模板实现的元编程框架

 

  Thread

  可移植的C++多线程库

 

  Python

  把C++类和函数映射到Python之中

  Pool

  内存池管理

 

  smart_ptr

  5个智能指针,学习智能指针必读,一份不错的参考是来自CUJ的文章:

  Smart Pointers in Boost,哦,这篇文章可以查到,CUJ是提供在线浏览的。
中文版见笔者在《Dr. Dobb's Journal软件研发杂志》第7辑上的译文。

  Boost总体来说是实用价值很高,质量很高的库。并且由于其对跨平台的强调,
对标准C++的强调,是编写平台无关,现代C++的开发者必备的工具。但是Boost中也
有很多是实验性质的东西,在实际的开发中实用需要谨慎。并且很多Boost中的库功
能堪称对语言功能的扩展,其构造用尽精巧的手法,不要贸然的花费时间研读。Bo
ost另外一面,比如Graph这样的库则是具有工业强度,结构良好,非常值得研读的
精品代码,并且也可以放心的在产品代码中多多利用。

  5.3 GUI

  在众多C++的库中,GUI部分的库算是比较繁荣,也比较引人注目的。在实际开
发中,GUI库的选择也是非常重要的一件事情,下面我们综述一下可选择的GUI库,
各自的特点以及相关工具的支持。

  (1) MFC

  大名鼎鼎的微软基础类库(Microsoft Foundation Class)。大凡学过VC++的
人都应该知道这个库。虽然从技术角度讲,MFC是不大漂亮的,但是它构建于Windo
ws API 之上,能够使程序员的工作更容易,编程效率高,减少了大量在建立 Windo
ws 程序时必须编写的代码,同时它还提供了所有一般 C++ 编程的优点,例如继承
和封装。MFC 编写的程序在各个版本的Windows操作系统上是可移植的,例如,在
Windows 3.1下编写的代码可以很容易地移植到 Windows NT 或 Windows 95 上。但
是在最近发展以及官方支持上日渐势微。

  (2) QT

  参考网站:http://www.trolltech.com/

  Qt是Trolltech公司的一个多平台的C++图形用户界面应用程序框架。它提供给
应用程序开发者建立艺术级的图形用户界面所需的所用功能。Qt是完全面向对象的
很容易扩展,并且允许真正地组件编程。自从1996年早些时候,Qt进入商业领域,
它已经成为全世界范围内数千种成功的应用程序的基础。Qt也是流行的Linux桌面环
境KDE 的基础,同时它还支持Windows、Macintosh、Unix/X11等多种平台。

  (3) WxWindows

  参考网站:http://www.wxwindows.org/

  跨平台的GUI库。因为其类层次极像MFC,所以有文章介绍从MFC到WxWindows的
代码移植以实现跨平台的功能。通过多年的开发也是一个日趋完善的GUI库,支持同
样不弱于前面两个库。并且是完全开放源代码的。新近的C++ Builder X的GUI设计
器就是基于这个库的。

  (4) Fox

  参考网站:http://www.fox-toolkit.org/

  开放源代码的GUI库。作者从自己亲身的开发经验中得出了一个理想的GUI库应
该是什么样子的感受出发,从而开始了对这个库的开发。有兴趣的可以尝试一下。


  (5) WTL

  基于ATL的一个库。因为使用了大量ATL的轻量级手法,模板等技术,在代码尺
寸,以及速度优化方面做得非常到位。主要面向的使用群体是开发COM轻量级供网络
下载的可视化控件的开发者。

  (6) GTK

  参考网站:http://gtkmm.sourceforge.net/

  GTK是一个大名鼎鼎的C的开源GUI库。在Linux世界中有Gnome这样的杀手应用。
而GTK就是这个库的C++封装版本。

  5.4 网络通信

  (1) ACE

  参考网站:http://www.cs.wustl.edu/~schmidt/ACE.html

  C++库的代表,超重量级的网络通信开发框架。ACE自适配通信环境(Adaptive
 Communication Environment)是可以自由使用、开放源代码的面向对象框架,在
其中实现了许多用于并发通信软件的核心模式。ACE提供了一组丰富的可复用C++包
装外观(Wrapper Facade)和框架组件,可跨越多种平台完成通用的通信软件任务
,其中包括:事件多路分离和事件处理器分派、信号处理、服务初始化、进程间通
信、共享内存管理、消息路由、分布式服务动态(重)配置、并发执行和同步,等
等。

  (2) StreamModule

  参考网站:http://www.omnifarious.org/StrMod/

  设计用于简化编写分布式程序的库。尝试着使得编写处理异步行为的程序更容
易,而不是用同步的外壳包起异步的本质。

  (3) SimpleSocket

  参考网站:http://home.hetnet.nl/~lcbokkers/simsock.htm

  这个类库让编写基于socket的客户/服务器程序更加容易。

  (4) A Stream Socket API for C++

  参考网站:http://www.pcs.cnu.edu/~dgame/sockets/socketsC++/sockets.h
tml

  又一个对Socket的封装库。

  5.5 XML

  (1) Xerces

  参考网站:http://xml.apache.org/xerces-c/

  Xerces-C++ 是一个非常健壮的XML解析器,它提供了验证,以及SAX和DOM API
。XML验证在文档类型定义(Document Type Definition,DTD)方面有很好的支持,
并且在2001年12月增加了支持W3C XML Schema 的基本完整的开放标准。

  (2) XMLBooster

  参考网站:http://www.xmlbooster.com/

  这个库通过产生特制的parser的办法极大的提高了XML解析的速度,并且能够产
生相应的GUI程序来修改这个parser。在DOM和SAX两大主流XML解析办法之外提供了
另外一个可行的解决方案。

  (3) Pull Parser

  参考网站:http://www.extreme.indiana.edu/xgws/xsoap/xpp/

  这个库采用pull方法的parser。在每个SAX的parser底层都有一个pull的parse
r,这个xpp把这层暴露出来直接给大家使用。在要充分考虑速度的时候值得尝试。


  (4) Xalan

  参考网站:http://xml.apache.org/xalan-c/

  Xalan是一个用于把XML文档转换为HTML,纯文本或者其他XML类型文档的XSLT处
理器。

  (5) CMarkup

  参考网站:http://www.firstobject.com/xml.htm

  这是一种使用EDOM的XML解析器。在很多思路上面非常灵活实用。值得大家在D
OM和SAX之外寻求一点灵感。

  (6) libxml++

  http://libxmlplusplus.sourceforge.net/

  libxml++是对著名的libxml XML解析器的C++封装版本

  5.6 科学计算

  (1) Blitz++

  参考网站:http://www.oonumerics.org/blitz/

  Blitz++ 是一个高效率的数值计算函数库,它的设计目的是希望建立一套既具
像C++ 一样方便,同时又比Fortran速度更快的数值计算环境。通常,用C++所写出
的数值程序,比 Fortran慢20%左右,因此Blitz++正是要改掉这个缺点。方法是利
用C++的template技术,程序执行甚至可以比Fortran更快。Blitz++目前仍在发展中
,对于常见的SVD,FFTs,QMRES等常见的线性代数方法并不提供,不过使用者可以
很容易地利用Blitz++所提供的函数来构建。

  (2) POOMA

  参考网站:http://www.codesourcery.com/pooma/pooma

  POOMA是一个免费的高性能的C++库,用于处理并行式科学计算。POOMA的面向对
象设计方便了快速的程序开发,对并行机器进行了优化以达到最高的效率,方便在
工业和研究环境中使用。

  (3) MTL

  参考网站:http://www.osl.iu.edu/research/mtl/

  Matrix Template Library(MTL)是一个高性能的泛型组件库,提供了各种格式
矩阵的大量线性代数方面的功能。在某些应用使用高性能编译器的情况下,比如In
tel的编译器,从产生的汇编代码可以看出其与手写几乎没有两样的效能。

  (4) CGAL

  参考网站:www.cgal.org

  Computational Geometry Algorithms Library的目的是把在计算几何方面的大
部分重要的解决方案和方法以C++库的形式提供给工业和学术界的用户。

  5.7 游戏开发

  (1) Audio/Video 3D C++ Programming Library

  参考网站:http://www.galacticasoftware.com/products/av/

  AV3D是一个跨平台,高性能的C++库。主要的特性是提供3D图形,声效支持(S
B,以及S3M),控制接口(键盘,鼠标和遥感),XMS。

  (2) KlayGE

  参考网站:http://home.g365.net/enginedev/

  国内游戏开发高手自己用C++开发的游戏引擎。KlayGE是一个开放源代码、跨平
台的游戏引擎,并使用Python作脚本语言。KlayGE在LGPL协议下发行。感谢龚敏敏
先生为中国游戏开发事业所做出的贡献。

  (3) OGRE

  参考网站:http://www.ogre3d.org

  OGRE(面向对象的图形渲染引擎)是用C++开发的,使用灵活的面向对象3D引擎
。它的目的是让开发者能更方便和直接地开发基于3D硬件设备的应用程序或游戏。
引擎中的类库对更底层的系统库(如:Direct3D和OpenGL)的全部使用细节进行了
抽象,并提供了基于现实世界对象的接口和其它类。

  5.8 线程

  (1) C++ Threads

  参考网站:http://threads.sourceforge.net/

  这个库的目标是给程序员提供易于使用的类,这些类被继承以提供在Linux环境
中很难看到的大量的线程方面的功能。

  (2) ZThreads

  参考网站:http://zthread.sourceforge.net/

  一个先进的面向对象,跨平台的C++线程和同步库。

  5.9 序列化

  (1) s11n

  参考网站:http://s11n.net/

  一个基于STL的C++库,用于序列化POD,STL容器以及用户定义的类型。

  (2) Simple XML Persistence Library

  参考网站:http://sxp.sourceforge.net/

  这是一个把对象序列化为XML的轻量级的C++库。

  5.10 字符串

  (1) C++ Str Library

  参考网站:http://www.utilitycode.com/str/

  操作字符串和字符的库,支持Windows和支持gcc的多种平台。提供高度优化的
代码,并且支持多线程环境和Unicode,同时还有正则表达式的支持。

  (2) Common Text Transformation Library

  参考网站:http://cttl.sourceforge.net/

  这是一个解析和修改STL字符串的库。CTTL substring类可以用来比较,插入,
替换以及用EBNF的语法进行解析。

  (3) GRETA

  参考网站:http://research.microsoft.com/projects/greta/

  这是由微软研究院的研究人员开发的处理正则表达式的库。在小型匹配的情况
下有非常优秀的表现。

  5.11 综合

  (1) P::Classes

  参考网站:http://pclasses.com/

  一个高度可移植的C++应用程序框架。当前关注类型和线程安全的signal/slot
机制,i/o系统包括基于插件的网络协议透明的i/o架构,基于插件的应用程序消息
日志框架,访问sql数据库的类等等。

  (2) ACDK - Artefaktur Component Development Kit

  参考网站:http://acdk.sourceforge.net/

  这是一个平台无关的C++组件框架,类似于Java或者.NET中的框架(反射机制,
线程,Unicode,废料收集,I/O,网络,实用工具,XML,等等),以及对Java, P
erl, Python, TCL, Lisp, COM 和 CORBA的集成。

  (3) dlib C++ library

  参考网站:http://www.cis.ohio-state.edu/~kingd/dlib/

  各种各样的类的一个综合。大整数,Socket,线程,GUI,容器类,以及浏览目
录的API等等。

  (4) Chilkat C++ Libraries

  参考网站:http://www.chilkatsoft.com/cpp_libraries.asp

  这是提供zip,e-mail,编码,S/MIME,XML等方面的库。

  (5) C++ Portable Types Library (PTypes)

  参考网站:http://www.melikyan.com/ptypes/

  这是STL的比较简单的替代品,以及可移植的多线程和网络库。

  (6) LFC

  参考网站:http://lfc.sourceforge.net/

  哦,这又是一个尝试提供一切的C++库

  5.12 其他库

  (1) Loki

  参考网站:http://www.moderncppdesign.com/

  哦,你可能抱怨我早该和Boost一起介绍它,一个实验性质的库。作者在loki中
把C++模板的功能发挥到了极致。并且尝试把类似设计模式这样思想层面的东西通过
库来提供。同时还提供了智能指针这样比较实用的功能。

  (2) ATL

  ATL(Active Template Library)

  是一组小巧、高效、灵活的类,这些类为创建可互操作的COM组件提供了基本的
设施。

  (3) FC++: The Functional C++ Library

  这个库提供了一些函数式语言中才有的要素。属于用库来扩充语言的一个代表
作。如果想要在OOP之外寻找另一分的乐趣,可以去看看函数式程序设计的世界。大
师Peter Norvig在 "Teach Yourself Programming in Ten Years"一文中就将函
数式语言列为至少应当学习的6类编程语言之一。

  (4) FACT!

  参考网站:http://www.kfa-juelich.de/zam/FACT/start/index.html

  另外一个实现函数式语言特性的库

  (5) Crypto++

  提供处理密码,消息验证,单向hash,公匙加密系统等功能的免费库。

  还有很多非常激动人心或者是极其实用的C++库,限于我们的水平以及文章的篇
幅不能包括进来。在对于这些已经包含近来的库的介绍中,由于并不是每一个我们
都使用过,所以难免有偏颇之处,请读者见谅。

------------------------------------------------------------------------
--------

  6,书籍

  以前熊节先生曾撰文评论相对于Java程序设计语言,C++的好书多如牛毛。荣耀
先生在《程序员》杂志上撰文《C++程序设计之四书五经》也将本领域内几乎所有的
经典书籍作了全面的介绍,任何关于书的评论此时看来便是很多余的了。个人浅见,
除非你打算以C++作为唯一兴趣或者生存之本,一般读者确实没有足够的时间和必要
将20余本书籍全部阅读。更有参考价值的是荣耀先生的另一篇文章:《至少应该阅
读的九本C++著作》,可以从下面的地址浏览到此文:

  http://www.royaloo.com/articles/articles_2003/9CppBooks.htm

  下面几本书对于走在C++初学之路上的读者是我们最愿意推荐给大家的:

  (1) 《C++ Primer》

  哦,也许你会抱怨我们为什么不先介绍TCPL,但对于走在学习之路上的入门者,
本书内容更为全面,更为详细易懂,我们称它为"C++的超级宝典"并不过分。配有
一本不错的习题解答《C++ Primer Answer Book》可以辅助你的学习之路。

  (2) 《Essential C++》

  如果说《C++ Primer》是C++领域的超级宝典,那么此书作为掌握C++的大局观
当之无愧。正如《.NET大局观》一书能够让读者全揽.NET,本书讲述了C++中最核心
的全部主题。书虽不厚,内容精炼,不失为《C++ Primer》读者茶余饭后的主题回
顾之作。

  (3) 《The C++ Programming Language》

  Bjarne为你带来的C++教程,真正能够告诉你怎么用才叫真正的C++的唯一一本
书。虽然如同"某某程序设计语言"这样的书籍会给大家一个内容全揽,入门到精
通的感觉,但本书确实不太适合初学者阅读。如果你自认为是一名很有经验的C++程
序员,那至少也要反复咀嚼Bjarne先生所强调的若干内容。

  (4) 《Effective C++》,《More Effective C++》

  是的,正如一些C++爱好者经常以读过与没有读过上述两本作品来区分你是否是
C++高手。我们也极力推崇这两本著作。在各种介绍C++专家经验的书籍里面,这两
本是最贴近语言本质,看后最能够有脱胎换骨感觉的书,读此书你需每日三省汝身

  技术书籍仁者见仁,过多的评论反无太多意义,由读者喜好选择最适合自己的
书方为上策。

------------------------------------------------------------------------
--------

  7,资源网站

  正如我们可以通过计算机历史上的重要人物了解计算机史的发展,C++相关人物
的网站也可以使我们得到最有价值的参考与借鉴,下面的人物我们认为没有介绍的
必要,只因下面的人物在C++领域的地位众所周知,我们只将相关的资源进行罗列以
供读者学习,他们有的工作于贝尔实验室,有的工作于知名编译器厂商,有的在不
断推进语言的标准化,有的为读者撰写了多部千古奇作……
  (1) Bjarne Stroustrup
  http://www.research.att.com/~bs/

  (2) Stanley B. Lippman
  http://blogs.msdn.com/slippman/
  中文版 http://www.zengyihome.net/slippman/index.htm

  (3) Scott Meyers
  http://www.aristeia.com/

  (4) David Musser
  http://www.cs.rpi.edu/~musser/

  (5) Bruce Eckel
  http://www.bruceeckel.com

  (6) Nicolai M. Josuttis
  http://www.josuttis.com/

  (7) Herb Sutter
  http://www.gotw.ca/

  (8) Andrei Alexandrescu
  http://www.coderncppdesign.com/

  (9) 侯捷先生
  http://www.jjhou.com

  (10) 孟岩先生
  先生繁忙于工作,痴迷于技术,暂无个人主页,关于先生的作品可以通过CSDN
的专栏和侯先生的主页访问到。

  (11) 荣耀先生
  http://www.royaloo.com/

  (12) 潘爱民先生
  http://www.icst.pku.edu.cn/panaimin/pam_homepage.htm

  除了上述大师的主页外,以下的综合类C++学习参考站点是我们非常愿意向大家
推荐的:

  (1) CodeProject
  http://www.codeproject.com

  (2) CodeGuru
  http://www.codeguru.com

  (3) Dr. Dobb's Journal
  http://www.ddj.com

  (4) C/C++ Users Journal
  http://www.cuj.com

  (5) C维视点
  http://www.c-view.org

  (6) allaboutprogram
  http://www.allaboutprogram.com
  其他资料

  (1) ISO IEC JTC1/SC22/WG21 - C++:标准C++的权威参考
  http://anubis.dkuug.dk/jtc1/sc22/wg21/

  (2) C++ FAQ LITE — Frequently Asked Questions: 最为全面的C++FAQ
  http://www.sunistudio.com/cppfaq/index.html
  C/C++ 新闻组:
  你不妨尝试从这里提问和回答问题,很多不错的Q&A资源......

  (1) .alt.comp.lang.learn.c-c++
  这个简单些,如果你和我一样是个菜鸟

  (2) .comp.lang.c++.moderated
    嗯,这个显然水平高一些

  (3) .comp.std.c++
  如果你需要讨论标准C++相关话题的话

------------------------------------------------------------------------
--------

  8,不得不写的结束语

  结束的时候也是总结现状,展望未来的时候。虽然C++从脱胎于C开始,一路艰
难坎坷的走过来,但是无论如何C++已经取得了工业基础的地位。文章列举的大量相关
资源就是最好的证明,而业界的大量用C++写成的产品代码以及大量的C++职业工程
师则是最直接的证明。同时,我们可以看到各个高校的计算机专业都开设有C++这门
课程,网络上对于C++的学习讨论也从来都没有停过。但是,在Java和.NET两大企业
开发平台的围攻下,给人的感觉是C++越来越"不行"了。

  C++在面向企业的软件开发中,在开发便捷性等方面的确要比Java和C#差很多,
其中一个问题是C++语言本身比较复杂,学习曲线比较陡峭,另外一个问题是C++标
准化的时间太长,丧失了很多的壮大机会,耗费了很多精力在厂商的之间的斗争上
,而C++的标准库离一个完善的程序开发框架还缺少太多太多的内容,各个第三方的
类库和框架又在一致性和完整性上没法和随平台提供的框架相提并论。难道C++真的
要退出历史舞台了?

  从C++目前的活跃程度,以及应用现状来说是完全能够肯定C++仍然是软件工业
的基础,也不会退出历史舞台的。另外从Boost,Loki这些库中我们也能够看到C++
的发展非常活跃,对于新技术新思维非常激进,C++仍然广泛受到关注。从ACE在高
性能通信领域的应用,以及MTL这样的库在数值计算领域的出色表现,我们可以看到
C++在高性能应用场合下的不可替代的作用,而嵌入式系统这样的内存受限开发平台
,比如Symbian OS上,C++已经发挥着并且将发挥更大的作用。可以预见的是以后的
软件无论上层的应用怎么变,它的底层核心都会是由C/C++这样的系统级软件编写的
,比如Java虚拟机,.NET Framwork。因为只有这样的系统级软件才能完全彻底的发
挥机器的功能。

  需要看到的是两个趋势,一个趋势是C++变得更加复杂,更加学院派,通过模板
等有潜力的语法因素构造越来越精巧的库成为了现代C++的热点,虽然在利用库实现
新的编程范式,乃至设计模式等方面很有开创意义,也确实产生了一些能够便捷开
发的工具,但是更多的是把C++变得更加强大,更加复杂,也更加难懂,似乎也更加
学院派,不得不说它正在向边缘化道路发展。另一个趋势是C++在主流的企业应用开
发中已经逐渐退出了,ERP这样的企业软件开发中基本上不会考虑C++,除非需要考
虑性能或者和遗留代码的集成这些因素。C++退守到系统级别语言,成为软件工业的
基础是大势所趋。然而反思一下,真的是退守么?自从STL出现,无数的人风起云涌
的开始支持C++,他们狂呼"我看到深夜消失了,目标软件工程的出现。我看到了可
维护的代码。"是的,STL在可维护性下做得如此出色。但是又怎样呢?STL为C++铺
平了现代软件工程的道路,而在上层应用程序软件开发领域这块场地早不单独属于
C++,很多程序设计语言都做得很出色,疯狂的支持者会毫不犹豫地说我们应当支持
C++,因为它是世界上最棒的语言。而坦率地说,你的腰杆真的那么硬么?也许只是
在逃避一些事实。C++是优秀的,这不可否认,STL的出现让C++一度走上了最辉煌的
时刻,然而现在看来……我的一位恩师曾言:真正能够将STL应用得淋漓尽致的人很
保守地说国内也不超过200人,或许不加入STL能够使C++向着它应当发展的方向发展
的更好,而现在看来,C++也应当回首到真正属于他的那一片圣地上……

------------------------------------------------------------------------
--------

参考资料

本文成文时参考了以下资源:

1、《程序员》2004年2月,3月,"C++ 程序设计之四书五经" 荣耀

2、水'木清华BBS C++版精华区

3、http://jjhou.csdn.net

4、http://www.royaloo.com

5、http://www.zengyihome.net

6、C/C++ 开发人员:充实您的 XML 工具箱 http://www-900.ibm.com/developerW
orks/cn/xml/x-ctlbx/index.shtml

20 novembre

GCC内嵌汇编之语法详解(转)

GCC内嵌汇编之语法详解
sclarkca 发表于 2006-9-15 17:29:00

内嵌汇编语法如下:
       __asm__(汇编语句模板: 输出部分: 输入部分: 破坏描述部分)
共四个部分:汇编语句模板,输出部分,输入部分,破坏描述部分,各部分使用“:”格开,汇编语句模板必不可少,其他三部分可选,如果使用了后面的部分,而前面部分为空,也需要用“:”格开,相应部分内容为空。例如:
             __asm__ __volatile__("cli": : :"memory")

1、汇编语句模板
    汇编语句模板由汇编语句序列组成,语句之间使用“;”、“\n”或“\n\t”分开。指令中的操作数可以使用占位符引用C语言变量,操作数占位符最多10个,名称如下:%0,%1,…,%9。指令中使用占位符表示的操作数,总被视为long型(4个字节),但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节。对字节操作可以显式的指明是低字节还是次字节。方法是在%和序号之间插入一个字母,“b”代表低字节,“h”代表高字节,例如:%h1

2、输出部分
    输出部分描述输出操作数,不同的操作数描述符之间用逗号格开,每个操作数描述符由限定字符串和C 语言变量组成。每个输出操作数的限定字符串必须包含“=”表示他是一个输出操作数。
例:
          __asm__ __volatile__("pushfl ; popl %0 ; cli":"=g" (x) )
描述符字符串表示对该变量的限制条件,这样GCC 就可以根据这些条件决定如何分配寄存器,如何产生必要的代码处理指令操作数与C表达式或C变量之间的联系。

3、输入部分
输入部分描述输入操作数,不同的操作数描述符之间使用逗号格开,每个操作数描述符由限定字符串和C语言表达式或者C语言变量组成。
例1 :
            __asm__ __volatile__ ("lidt %0" : : "m" (real_mode_idt));
例二(bitops.h):


Static __inline__ void __set_bit(int nr, volatile void * addr)
{
        __asm__(
                         "btsl %1,%0"
                        :"=m" (ADDR)
                        :"Ir" (nr));
}

后例功能是将(*addr)的第nr位设为1。第一个占位符%0与C 语言变量ADDR对应,第二个占位符%1与C语言变量nr对应。因此上面的汇编语句代码与下面的伪代码等价:btsl nr, ADDR,该指令的两个操作数不能全是内存变量,因此将nr的限定字符串指定为“Ir”,将nr 与立即数或者寄存器相关联,这样两个操作数中只有ADDR为内存变量。

4、限制字符
   4.1、限制字符列表
   限制字符有很多种,有些是与特定体系结构相关,此处仅列出常用的限定字符和i386中可能用到的一些常用的限定符。它们的作用是指示编译器如何处理其后的C语言变量与指令操作数之间的关系。

   分类            限定符                    描述
  通用寄存器       “a”               将输入变量放入eax
                                              这里有一个问题:假设eax已经被使用,那怎么办?
                                 其实很简单:因为GCC 知道eax 已经被使用,它在这段汇编代码
                                 的起始处插入一条语句pushl %eax,将eax 内容保存到堆栈,然
                                 后在这段代码结束处再增加一条语句popl %eax,恢复eax的内容
                   “b”               将输入变量放入ebx
                             “c”               将输入变量放入ecx
                            “d”                将输入变量放入edx
                             “s”               将输入变量放入esi
                            “d”               将输入变量放入edi
                             “q”              将输入变量放入eax,ebx,ecx,edx中的一个
                   “r”               将输入变量放入通用寄存器,也就是eax,ebx,ecx,
                                        edx,esi,edi中的一个
                    “A”              把eax和edx合成一个64 位的寄存器(use long longs)

       内存            “m”             内存变量
                     “o”             操作数为内存变量,但是其寻址方式是偏移量类型,
                                       也即是基址寻址,或者是基址加变址寻址
                     “V”             操作数为内存变量,但寻址方式不是偏移量类型
                     “ ”             操作数为内存变量,但寻址方式为自动增量
                     “p”             操作数是一个合法的内存地址(指针)

     寄存器或内存    “g”             将输入变量放入eax,ebx,ecx,edx中的一个
                                       或者作为内存变量
                      “X”            操作数可以是任何类型

     立即数
                     “I”             0-31之间的立即数(用于32位移位指令)
                      “J”            0-63之间的立即数(用于64位移位指令)
                     “N”             0-255之间的立即数(用于out指令)
                     “i”             立即数  
                     “n”            立即数,有些系统不支持除字以外的立即数,
                                       这些系统应该使用“n”而不是“i”

     匹配            “ 0 ”,         表示用它限制的操作数与某个指定的操作数匹配,
                     “1” ...               也即该操作数就是指定的那个操作数,例如“0”
                      “9”            去描述“%1”操作数,那么“%1”引用的其实就
                                       是“%0”操作数,注意作为限定符字母的0-9 与
                                       指令中的“%0”-“%9”的区别,前者描述操作数,
                                       后者代表操作数。
                       &                     该输出操作数不能使用过和输入操作数相同的寄存器

    操作数类型        “=”          操作数在指令中是只写的(输出操作数)  
                       “+”          操作数在指令中是读写类型的(输入输出操作数)

     浮点数            “f”          浮点寄存器
                      “t”           第一个浮点寄存器
                      “u”          第二个浮点寄存器
                      “G”          标准的80387浮点常数
                       %                   该操作数可以和下一个操作数交换位置
                                       例如addl的两个操作数可以交换顺序
                                      (当然两个操作数都不能是立即数)
                       #                   部分注释,从该字符到其后的逗号之间所有字母被忽略
                       *                     表示如果选用寄存器,则其后的字母被忽略

5、破坏描述部分
   破坏描述符用于通知编译器我们使用了哪些寄存器或内存,由逗号格开的字符串组成,每个字符串描述一种情况,一般是寄存器名;除寄存器外还有“memory”。例如:“%eax”,“%ebx”,“memory”等。

15 novembre

C++内存管理1(转载,下同)

[导语]

   内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对C++的痛恨,但内存管理在C++中无处不在,内存泄漏几乎在每个C++程序中都会发生,因此要想成为C++高手,内存管理一关是必须要过的,除非放弃C++,转到Java或者.NET,他们的内存管理基本是自动的,当然你也放弃了自由和对内存的支配权,还放弃了C++超绝的性能。本期专题将从内存管理、内存泄漏、内存回收这三个方面来探讨C++内存管理问题。

1 内存管理

   伟大的Bill Gates 曾经失言: 

  640K ought to be enough for everybody — Bill Gates 1981  

   程序员们经常编写内存管理程序,往往提心吊胆。如果不想触雷,唯一的解决办法就是发现所有潜伏的地雷并且排除它们,躲是躲不了的。本文的内容比一般教科书的要深入得多,读者需细心阅读,做到真正地通晓内存管理。

1.1 C++内存管理详解

1.1.1 内存分配方式

1.1.1.1 分配方式简介

  在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局 /静态存储区和常量存储区。  

  栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 

  堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new 就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。  

  自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free 来结束自己的生命的。  

  全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C 语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。  

  常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。

1.1.1.2 明确区分堆与栈

  在bbs上,堆与栈的区分问题,似乎是一个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我决定拿他第一个开刀。  

  首先,我们举一个例子:

    void f() { int* p=new int[5]; }
 
 

  这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p 呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new 分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:  

00401028 push 14h

0040102A call operator new (00401060)

0040102F add esp,4

00401032 mov dword ptr [ebp-8],eax

00401035 mov eax,dword ptr [ebp-8]

00401038 mov dword ptr [ebp-4],eax

 

  这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete []p ,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie 信息去进行释放内存的工作。

1.1.1.3 堆和栈究竟有什么区别?

  好了,我们回到我们的主题:堆和栈究竟有什么区别?  

  主要的区别由以下几点:  

  1、管理方式不同;

  2、空间大小不同;

  3、能否产生碎片不同;

  4、生长方向不同;

  5、分配方式不同;

  6、分配效率不同;  

  管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak  

  空间大小:一般来讲在32位系统下,堆内存可以达到4G 的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M (好像是,记不清楚了)。当然,我们可以修改:  

  打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和 commit  

  注意:reserve最小值为4Bytecommit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。  

  碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。  

  生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。  

  分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。  

  分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构 /操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。 

  从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址, EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。  

  虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。  

   无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候 debug可是相当困难的:)

1.1.2 控制C++的内存分配

  在嵌入式系统中使用C++的一个常见问题是内存分配,即对new delete 操作符的失控。

  具有讽刺意味的是,问题的根源却是C++对内存的管理非常的容易而且安全。具体地说,当一个对象被消除时,它的析构函数能够安全的释放所分配的内存。  

  这当然是个好事情,但是这种使用的简单性使得程序员们过度使用new delete ,而不注意在嵌入式C++环境中的因果关系。并且,在嵌入式系统中,由于内存的限制,频繁的动态分配不定大小的内存会引起很大的问题以及堆破碎的风险。 

  作为忠告,保守的使用内存分配是嵌入式环境中的第一原则。 

  但当你必须要使用new delete时,你不得不控制 C++中的内存分配。你需要用一个全局的new delete来代替系统的内存分配符,并且一个类一个类的重载new delete 

  一个防止堆破碎的通用方法是从不同固定大小的内存持中分配不同类型的对象。对每个类重载new delete 就提供了这样的控制。

1.1.2.1 重载全局的 new delete操作符

  可以很容易地重载new delete 操作符,如下所示: 

void * operator new(size_t size)

{

void *p = malloc(size);

return (p);

}

void operator delete(void *p);

{

free(p);

 

  这段代码可以代替默认的操作符来满足内存分配的请求。出于解释C++的目的,我们也可以直接调用malloc() free() 

  也可以对单个类的new delete 操作符重载。这是你能灵活的控制对象的内存分配。 

class TestClass {

public:

void * operator new(size_t size);

void operator delete(void *p);

// .. other members here ...

}; 

void *TestClass::operator new(size_t size)

{

void *p = malloc(size); // Replace this with alternative allocator

return (p);

}

void TestClass::operator delete(void *p)

{

free(p); // Replace this with alternative de-allocator

}

 

  所有TestClass 对象的内存分配都采用这段代码。更进一步,任何从TestClass 继承的类也都采用这一方式,除非它自己也重载了new delete 操作符。通过重载 new delete 操作符的方法,你可以自由地采用不同的分配策略,从不同的内存池中分配不同的类对象。

1.1.2.2 为单个的类重载 new[ ] delete[ ]

  必须小心对象数组的分配。你可能希望调用到被你重载过的new delete 操作符,但并不如此。内存的请求被定向到全局的new[ ]delete[ ] 操作符,而这些内存来自于系统堆。 

  C++将对象数组的内存分配作为一个单独的操作,而不同于单个对象的内存分配。为了改变这种方式,你同样需要重载 new[ ] delete[ ]操作符。 

class TestClass {

public:

void * operator new[ ](size_t size);

void operator delete[ ](void *p);

// .. other members here ..

};

void *TestClass::operator new[ ](size_t size)

{

void *p = malloc(size);

return (p);

}

void TestClass::operator delete[ ](void *p)

{

free(p);

}

int main(void)

{

TestClass *p = new TestClass[10]; 

// ... etc ... 

delete[ ] p;

但是注意:对于多数C++的实现,new[]操作符中的个数参数是数组的大小加上额外的存储对象数目的一些字节。在你的内存分配机制重要考虑的这一点。你应该尽量避免分配对象数组,从而使你的内存分配策略简单。

1.1.3 常见的内存错误及其对策

   发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。 常见的内存错误及其对策如下: 

  * 内存分配未成功,却使用了它。 

  编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针 p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行 

  检查。如果是用mallocnew来申请内存,应该用 if(p==NULL) if(p!=NULL)进行防错处理。 

  * 内存分配虽然成功,但是尚未初始化就引用它。 

  犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。 内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。  

  * 内存分配成功并且已经初始化,但操作越过了内存的边界。 

  例如在使用数组时经常发生下标"多1"或者"少1" 的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。 

  * 忘记了释放内存,造成内存泄露。 

  含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。 

  动态内存的申请与释放必须配对,程序中mallocfree 的使用次数一定要相同,否则肯定有错误(new/delete同理)。 

  * 释放了内存却继续使用它。

 

  有三种情况: 

  (1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。 

  (2)函数的return语句写错了,注意不要返回指向"栈内存"的"指针"或者"引用",因为该内存在函数体结束时被自动销毁。  

  (3)使用free delete释放了内存后,没有将指针设置为NULL。导致产生"野指针"。 

  【规则1】用malloc new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为 NULL的内存。 

  【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。 

  【规则3】避免数组或指针的下标越界,特别要当心发生"多1" 或者"少1"操作。 

  【规则4】动态内存的申请与释放必须配对,防止内存泄漏。 

  【规则5】用free delete释放了内存之后,立即将指针设置为NULL,防止产生"野指针"。

1.1.4 指针与数组的对比

  C++/C程序中,指针和数组在不少地方可以相互替换着用,让人产生一种错觉,以为两者是等价的。 

  数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。 

  指针可以随时指向任意类型的内存块,它的特征是"可变",所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。 

  下面以字符串为例比较指针与数组的特性。

1.1.4.1 修改内容

   下面示例中,字符数组a的容量是6个字符,其内容为 helloa的内容可以改变,如a[0]= 'X' 。指针p指向常量字符串"world"(位于静态存储区,内容为 world),常量字符串的内容是不可以被修改的。从语法上看,编译器并不觉得语句p[0]= 'X'有什么不妥,但是该语句企图修改常量字符串的内容而导致运行错误。  

   
char a[] = "hello";

a[0] = 'X';

cout << a << endl;

char *p = "world"; // 注意p指向常量字符串

p[0] = 'X'; // 编译器不能发现该错误

cout << p << endl;

1.1.4.2 内容复制与比较

  不能对数组名进行直接复制与比较。若想把数组a的内容复制给数组b ,不能用语句 b = a ,否则将产生编译错误。应该用标准库函数strcpy进行复制。同理,比较 ba的内容是否相同,不能用if(b==a) 来判断,应该用标准库函数 strcmp进行比较。

   语句p = a 并不能把a的内容复制指针 p,而是把a的地址赋给了p 。要想复制a的内容,可以先用库函数malloc p申请一块容量为strlen(a)+1个字符的内存,再用strcpy 进行字符串复制。同理,语句 if(p==a) 比较的不是内容而是地址,应该用库函数strcmp来比较。 

   
// 数组…

char a[] = "hello";

char b[10];

strcpy(b, a); // 不能用 b = a;

if(strcmp(b, a) == 0) // 不能用 if (b == a)

// 指针…

int len = strlen(a);

char *p = (char *)malloc(sizeof(char)*(len+1));

strcpy(p,a); // 不要用 p = a;

if(strcmp(p, a) == 0) // 不要用 if (p == a)

1.1.4.3 计算内存容量

   用运算符sizeof可以计算出数组的容量(字节数)。如下示例中,sizeof(a) 的值是12(注意别忘了'')。指针p指向a ,但是sizeof(p)的值却是4。这是因为 sizeof(p)得到的是一个指针变量的字节数,相当于sizeof(char*),而不是p 所指的内存容量。C++/C语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。 

char a[] = "hello world";

char *p = a;

cout<< sizeof(a) << endl; // 12字节

cout<< sizeof(p) << endl; // 4字节

  

   注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。如下示例中,不论数组a的容量是多少, sizeof(a)始终等于sizeof(char *) 

void Func(char a[100])

{

 cout<< sizeof(a) << endl; // 4字节而不是100 字节

}

C++内存管理2


1.1.5 指针参数是如何传递内存的?

   如果函数的参数是一个指针,不要指望用该指针去申请动态内存。如下示例中,Test函数的语句GetMemory(str, 200) 并没有使str获得期望的内存,str依旧是 NULL,为什么?

   
void GetMemory(char *p, int num)

{

 p = (char *)malloc(sizeof(char) * num);

}

void Test(void)

{

 char *str = NULL;

 GetMemory(str, 100); // str 仍然为 NULL

 strcpy(str, "hello"); // 运行错误

}

 

   毛病出在函数GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数p 的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了 _p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p 申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以函数 GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free 释放内存。 

   如果非得要用指针参数去申请内存,那么应该改用"指向指针的指针",见示例:

   
void GetMemory2(char **p, int num)

{

 *p = (char *)malloc(sizeof(char) * num);

}

void Test2(void)

{

 char *str = NULL;

 GetMemory2(&str, 100); // 注意参数是 &str ,而不是str

 strcpy(str, "hello");

 cout<< str << endl;

 free(str);

}

 

   由于"指向指针的指针"这个概念不容易理解,我们可以用函数返回值来传递动态内存。这种方法更加简单,见示例:

   
char *GetMemory3(int num)

{

 char *p = (char *)malloc(sizeof(char) * num);

 return p;

}

void Test3(void)

{

 char *str = NULL;

 str = GetMemory3(100);

 strcpy(str, "hello");

 cout<< str << endl;

 free(str);

}

 

   用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把return语句用错了。这里强调不要用return 语句返回指向"栈内存"的指针,因为该内存在函数结束时自动消亡,见示例:

   
char *GetString(void)

{

 char p[] = "hello world";

 return p; // 编译器将提出警告

}

void Test4(void)

{

 char *str = NULL;

 str = GetString(); // str 的内容是垃圾

 cout<< str << endl;

}

 

   用调试器逐步跟踪Test4,发现执行str = GetString 语句后str不再是NULL指针,但是str 的内容不是"hello world"而是垃圾。

   如果把上述示例改写成如下示例,会怎么样?

   
char *GetString2(void)

{

 char *p = "hello world";

 return p;

}

void Test5(void)

{

 char *str = NULL;

 str = GetString2();

 cout<< str << endl;

}

 

   函数Test5运行虽然不会出错,但是函数GetString2 的设计概念却是错误的。因为GetString2内的"hello world"是常量字符串,位于静态存储区,它在程序生命期内恒定不变。无论什么时候调用 GetString2,它返回的始终是同一个"只读"的内存块。

1.1.6 杜绝"野指针"

  "野指针"不是NULL指针,是指向"垃圾"内存的指针。人们一般不会错用 NULL 指针,因为用if语句很容易判断。但是"野指针"是很危险的,if 语句对它不起作用。 "野指针"的成因主要有两种: 

   1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL 指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如

   
char *p = NULL;

char *str = (char *) malloc(100);

 

   2)指针p free或者delete之后,没有置为NULL,让人误以为 p是个合法的指针。

   3)指针操作超越了变量的作用域范围。这种情况让人防不胜防,示例程序如下:

   
class A

{

 public:

  void Func(void){ cout << "Func of class A" << endl; }

};

void Test(void)

{

 A *p;

 {

  A a;

  p = &a; // 注意 a 的生命期

 }

 p->Func(); // p是"野指针"

}

 

   函数Test在执行语句p->Func() 时,对象a已经消失,而p是指向a 的,所以p就成了"野指针"。但奇怪的是我运行这个程序时居然没有出错,这可能与编译器有关。

1.1.7 有了malloc/free为什么还要 new/delete

  mallocfree C++/C语言的标准库函数,new/deleteC++的运算符。它们都可用于申请动态内存和释放内存。  

  对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于 malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free 

    因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new ,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。我们先看一看 malloc/freenew/delete如何实现对象的动态内存管理,见示例:

    
class Obj

{

 public :

  Obj(void){ cout << "Initialization" << endl; }

  ~Obj(void){ cout << "Destroy" << endl; }

  void Initialize(void){ cout << "Initialization" << endl; }

  void Destroy(void){ cout << "Destroy" << endl; }

};

void UseMallocFree(void)

{

 Obj *a = (obj *)malloc(sizeof(obj)); // 申请动态内存

 a->Initialize(); // 初始化

 //…

 a->Destroy(); // 清除工作

 free(a); // 释放内存

}

void UseNewDelete(void)

{

 Obj *a = new Obj; // 申请动态内存并且初始化

 //…

 delete a; // 清除并且释放内存

}

 

  类Obj的函数Initialize模拟了构造函数的功能,函数 Destroy模拟了析构函数的功能。函数UseMallocFree中,由于 malloc/free不能执行构造函数与析构函数,必须调用成员函数InitializeDestroy 来完成初始化与清除工作。函数UseNewDelete则简单得多。 

  所以我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete 。由于内部数据类型的"对象"没有构造与析构的过程,对它们而言malloc/freenew/delete 是等价的。 

  既然new/delete的功能完全覆盖了malloc/free ,为什么C++不把malloc/free淘汰出局呢?这是因为 C++程序经常要调用C函数,而C程序只能用 malloc/free管理动态内存。 

   如果用free释放"new创建的动态对象",那么该对象因无法执行析构函数而可能导致程序出错。如果用 delete释放"malloc申请的动态内存",结果也会导致程序出错,但是该程序的可读性很差。所以 new/delete必须配对使用,malloc/free也一样。

1.1.8 内存耗尽怎么办?

  如果在申请动态内存时找不到足够大的内存块,mallocnew 将返回NULL指针,宣告内存申请失败。通常有三种方式处理"内存耗尽"问题。 

  (1)判断指针是否为NULL,如果是则马上用 return语句终止本函数。例如:

void Func(void)

{

 A *a = new A;

 if(a == NULL)

 {

  return;

 }

 

}

 

 (2)判断指针是否为NULL,如果是则马上用 exit(1)终止整个程序的运行。例如:

void Func(void)

{

 A *a = new A;

 if(a == NULL)

 {

  cout << "Memory Exhausted" << endl;

  exit(1);

 }

 

}

 

  (3)为new malloc设置异常处理函数。例如Visual C++可以用_set_new_hander 函数为new设置用户自己定义的异常处理函数,也可以让malloc享用与 new相同的异常处理函数。详细内容请参考C++使用手册。 

  上述(1)(2)方式使用最普遍。如果一个函数内有多处需要申请动态内存,那么方式( 1)就显得力不从心(释放内存很麻烦),应该用方式(2)来处理。 

  很多人不忍心用exit(1),问:"不编写出错处理程序,让操作系统自己解决行不行?" 

  不行。如果发生"内存耗尽"这样的事情,一般说来应用程序已经无药可救。如果不用exit(1) 把坏程序杀死,它可能会害死操作系统。道理如同:如果不把歹徒击毙,歹徒在老死之前会犯下更多的罪。  

  有一个很重要的现象要告诉大家。对于32位以上的应用程序而言,无论怎样使用mallocnew,几乎不可能导致"内存耗尽"。我在Windows 98下用 Visual C++编写了测试程序,见示例7。这个程序会无休止地运行下去,根本不会终止。因为32 位操作系统支持"虚存",内存用完了,自动用硬盘空间顶替。我只听到硬盘嘎吱嘎吱地响,Window 98已经累得对键盘、鼠标毫无反应。 

  我可以得出这么一个结论:对于32位以上的应用程序,"内存耗尽"错误处理程序毫无用处。这下可把 Unix Windows程序员们乐坏了:反正错误处理程序不起作用,我就不写了,省了很多麻烦。 

   我不想误导读者,必须强调:不加错误处理将导致程序的质量很差,千万不可因小失大。

   
void main(void)

{

 float *p = NULL;

 while(TRUE)

 {

  p = new float[1000000];

  cout << "eat memory" << endl;

  if(p==NULL)

   exit(1);

 }

}

1.1.9 malloc/free的使用要点

   函数malloc的原型如下:

   
void * malloc(size_t size);
 

   malloc申请一块长度为length 的整数类型的内存,程序如下:

   
int *p = (int *) malloc(sizeof(int) * length);
 

   我们应当把注意力集中在两个要素上:"类型转换"和"sizeof"

   * malloc返回值的类型是void *,所以在调用 malloc时要显式地进行类型转换,将void * 转换成所需要的指针类型。 

   * malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。我们通常记不住int, float 等数据类型的变量的确切字节数。例如int变量在16位系统下是 2个字节,在32位下是4个字节;而 float变量在16位系统下是4个字节,在 32位下也是4个字节。最好用以下程序作一次测试:

   
cout << sizeof(char) << endl;

cout << sizeof(int) << endl;

cout << sizeof(unsigned int) << endl;

cout << sizeof(long) << endl;

cout << sizeof(unsigned long) << endl;

cout << sizeof(float) << endl;

cout << sizeof(double) << endl;

cout << sizeof(void *) << endl;

   malloc的"()" 中使用 sizeof运算符是良好的风格,但要当心有时我们会昏了头,写出 p = malloc(sizeof(p))这样的程序来。  

   函数free的原型如下:

   
void free( void * memblock );

   为什么free函数不象malloc函数那样复杂呢?这是因为指针 p的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果 pNULL指针,那么free p无论操作多少次都不会出问题。如果p不是NULL指针,那么 freep连续操作两次就会导致程序运行错误。

1.1.10 new/delete的使用要点

   运算符new使用起来要比函数malloc 简单得多,例如:

   
int *p1 = (int *)malloc(sizeof(int) * length);

int *p2 = new int[length];

   这是因为new内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言, new在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new的语句也可以有多种形式。例如

   
class Obj

{

 public :

  Obj(void); // 无参数的构造函数

  Obj(int x); // 带一个参数的构造函数

  …

}

void Test(void)

{

 Obj *a = new Obj;

 Obj *b = new Obj(1); // 初值为1

 

 delete a;

 delete b;

}

 

   如果用new创建对象数组,那么只能使用对象的无参数构造函数。例如:

   
Obj *objects = new Obj[100]; // 创建100个动态对象
 

   不能写成:

   
Obj *objects = new Obj[100](1);// 创建100 个动态对象的同时赋初值1
 

   在用delete释放对象数组时,留意不要丢了符号'[]' 。例如:

   
delete []objects; // 正确的用法

delete objects; // 错误的用法

 

   后者有可能引起程序崩溃和内存泄漏。

C++内存管理3

1.2 C++中的健壮指针和资源管理

  我最喜欢的对资源的定义是:"任何在你的程序中获得并在此后释放的东西?quot; 内存是一个相当明显的资源的例子。它需要用new来获得,用delete来释放。同时也有许多其它类型的资源文件句柄、重要的片断、 Windows中的GDI资源,等等。将资源的概念推广到程序中创建、释放的所有对象也是十分方便的,无论对象是在堆中分配的还是在栈中或者是在全局作用于内生命的。  

  对于给定的资源的拥有着,是负责释放资源的一个对象或者是一段代码。所有权分立为两种级别——自动的和显式的(automatic and explicit),如果一个对象的释放是由语言本身的机制来保证的,这个对象的就是被自动地所有。例如,一个嵌入在其他对象中的对象,他的清除需要其他对象来在清除的时候保证。外面的对象被看作嵌入类的所有者。   类似地,每个在栈上创建的对象(作为自动变量)的释放(破坏)是在控制流离开了对象被定义的作用域的时候保证的。这种情况下,作用于被看作是对象的所有者。注意所有的自动所有权都是和语言的其他机制相容的,包括异常。无论是如何退出作用域的——正常流程控制退出、一个 break语句、一个return、一个goto、或者是一个 throw——自动资源都可以被清除。 

  到目前为止,一切都很好!问题是在引入指针、句柄和抽象的时候产生的。如果通过一个指针访问一个对象的话,比如对象在堆中分配,C++不自动地关注它的释放。程序员必须明确的用适当的程序方法来释放这些资源。比如说,如果一个对象是通过调用 new来创建的,它需要用delete来回收。一个文件是用 CreateFile(Win32 API)打开的,它需要用CloseHandle来关闭。用EnterCritialSection 进入的临界区(Critical Section)需要LeaveCriticalSection 退出,等等。一个""指针,文件句柄,或者临界区状态没有所有者来确保它们的最终释放。基本的资源管理的前提就是确保每个资源都有他们的所有者。

1.2.1 第一条规则(RAII

  一个指针,一个句柄,一个临界区状态只有在我们将它们封装入对象的时候才会拥有所有者。这就是我们的第一规则:在构造函数中分配资源,在析构函数中释放资源。 

  当你按照规则将所有资源封装的时候,你可以保证你的程序中没有任何的资源泄露。这点在当封装对象(Encapsulating Object)在栈中建立或者嵌入在其他的对象中的时候非常明显。但是对那些动态申请的对象呢?不要急!任何动态申请的东西都被看作一种资源,并且要按照上面提到的方法进行封装。这一对象封装对象的链不得不在某个地方终止。它最终终止在最高级的所有者,自动的或者是静态的。这些分别是对离开作用域或者程序时释放资源的保证。  

  下面是资源封装的一个经典例子。在一个多线程的应用程序中,线程之间共享对象的问题是通过用这样一个对象联系临界区来解决的。每一个需要访问共享资源的客户需要获得临界区。例如,这可能是Win32下临界区的实现方法。  

class CritSect

{

 friend class Lock;

 public:

  CritSect () { InitializeCriticalSection (&_critSection); }

  ~CritSect () { DeleteCriticalSection (&_critSection); }

 private:

  void Acquire ()

  {

   EnterCriticalSection (&_critSection);

  }

  void Release ()

  {

   LeaveCriticalSection (&_critSection);

  } 

 private:

  CRITICAL_SECTION _critSection;

};

 

  这里聪明的部分是我们确保每一个进入临界区的客户最后都可以离开。"进入" 临界区的状态是一种资源,并应当被封装。封装器通常被称作一个锁(lock)。 

class Lock

{

 public:

  Lock (CritSect& critSect) : _critSect (critSect)

  {

   _critSect.Acquire ();

  }

  ~Lock ()

  {

   _critSect.Release ();

  }

 private

  CritSect & _critSect;

};

 

  锁一般的用法如下:

void Shared::Act () throw (char *)

{

 Lock lock (_critSect);

 // perform action —— may throw

 // automatic destructor of lock

}

 

  注意无论发生什么,临界区都会借助于语言的机制保证释放。  

  还有一件需要记住的事情——每一种资源都需要被分别封装。这是因为资源分配是一个非常容易出错的操作,是要资源是有限提供的。我们会假设一个失败的资源分配会导致一个异常——事实上,这会经常的发生。所以如果你想试图用一个石头打两只鸟的话,或者在一个构造函数中申请两种形式的资源,你可能就会陷入麻烦。只要想想在一种资源分配成功但另一种失败抛出异常时会发生什么。因为构造函数还没有全部完成,析构函数不可能被调用,第一种资源就会发生泄露。  

   这种情况可以非常简单的避免。无论何时你有一个需要两种以上资源的类时,写两个小的封装器将它们嵌入你的类中。每一个嵌入的构造都可以保证删除,即使包装类没有构造完成。

1.2.2 Smart Pointers

  我们至今还没有讨论最常见类型的资源——用操作符new分配,此后用指针访问的一个对象。我们需要为每个对象分别定义一个封装类吗?(事实上, C++标准模板库已经有了一个模板类,叫做auto_ptr,其作用就是提供这种封装。我们一会儿在回到auto_ptr 。)让我们从一个极其简单、呆板但安全的东西开始。看下面的Smart Pointer模板类,它十分坚固,甚至无法实现。 

template <class T>

class SmartPointer

{

 public:

  ~SmartPointer () { delete _p; }

  T * operator->() { return _p; }

  T const * operator->() const { return _p; }

 protected:

  SmartPointer (): _p (0) {}

  explicit SmartPointer (T* p): _p (p) {}

  T * _p;

};

 

  为什么要把SmartPointer的构造函数设计为protected 呢?如果我需要遵守第一条规则,那么我就必须这样做。资源——在这里是class T的一个对象——必须在封装器的构造函数中分配。但是我不能只简单的调用new T ,因为我不知道T的构造函数的参数。因为,在原则上,每一个T都有一个不同的构造函数;我需要为他定义个另外一个封装器。模板的用处会很大,为每一个新的类,我可以通过继承 SmartPointer定义一个新的封装器,并且提供一个特定的构造函数。 

class SmartItem: public SmartPointer<Item>

{

 public:

  explicit SmartItem (int i)

  : SmartPointer<Item> (new Item (i)) {}

};

 

  为每一个类提供一个Smart Pointer真的值得吗?说实话——不!他很有教学的价值,但是一旦你学会如何遵循第一规则的话,你就可以放松规则并使用一些高级的技术。这一技术是让 SmartPointer的构造函数成为public,但是只是是用它来做资源转换(Resource Transfer )我的意思是用new操作符的结果直接作为SmartPointer的构造函数的参数,像这样: 

SmartPointer<Item> item (new Item (i));
 

  这个方法明显更需要自控性,不只是你,而且包括你的程序小组的每个成员。他们都必须发誓出了作资源转换外不把构造函数用在人以其他用途。幸运的是,这条规矩很容易得以加强。只需要在源文件中查找所有的new 即可。

1.2.3 Resource Transfer

  到目前为止,我们所讨论的一直是生命周期在一个单独的作用域内的资源。现在我们要解决一个困难的问题——如何在不同的作用域间安全的传递资源。这一问题在当你处理容器的时候会变得十分明显。你可以动态的创建一串对象,将它们存放至一个容器中,然后将它们取出,并且在最终安排它们。为了能够让这安全的工作——没有泄露——对象需要改变其所有者。 

  这个问题的一个非常显而易见的解决方法是使用Smart Pointer,无论是在加入容器前还是还找到它们以后。这是他如何运作的,你加入 Release方法到Smart Pointer中: 

template <class T>

T * SmartPointer<T>::Release ()

{

T * pTmp = _p;

_p = 0;

return pTmp;

}

 

  注意在Release调用以后,Smart Pointer 就不再是对象的所有者了——它内部的指针指向空。现在,调用了Release都必须是一个负责的人并且迅速隐藏返回的指针到新的所有者对象中。在我们的例子中,容器调用了Release ,比如这个Stack的例子: 

void Stack::Push (SmartPointer <Item> & item) throw (char *)

{

if (_top == maxStack)

throw "Stack overflow";

_arr [_top++] = item.Release ();

};

 

  同样的,你也可以再你的代码中用加强Release的可靠性。 

   相应的Pop方法要做些什么呢?他应该释放了资源并祈祷调用它的是一个负责的人而且立即作一个资源传递它到一个 Smart Pointer?这听起来并不好。

1.2.4 Strong Pointers

 

  资源管理在内容索引(Windows NT Server上的一部分,现在是Windows 2000 )上工作,并且,我对这十分满意。然后我开始想……这一方法是在这样一个完整的系统中形成的,如果可以把它内建入语言的本身岂不是一件非常好?我提出了强指针(Strong Pointer)和弱指针 (Weak Pointer)。一个Strong Pointer会在许多地方和我们这个SmartPointer 相似--它在超出它的作用域后会清除他所指向的对象。资源传递会以强指针赋值的形式进行。也可以有Weak Pointer存在,它们用来访问对象而不需要所有对象 --比如可赋值的引用。 

  任何指针都必须声明为Strong或者Weak ,并且语言应该来关注类型转换的规定。例如,你不可以将Weak Pointer传递到一个需要Strong Pointer的地方,但是相反却可以。 Push方法可以接受一个Strong Pointer并且将它转移到 Stack中的Strong Pointer的序列中。Pop方法将会返回一个 Strong Pointer。把Strong Pointer的引入语言将会使垃圾回收成为历史。 

  这里还有一个小问题--修改C++标准几乎和竞选美国总统一样容易。当我将我的注意告诉给 Bjarne Stroutrup的时候,他看我的眼神好像是我刚刚要向他借一千美元一样。 

   然后我突然想到一个念头。我可以自己实现Strong Pointers。毕竟,它们都很想Smart Pointers 。给它们一个拷贝构造函数并重载赋值操作符并不是一个大问题。事实上,这正是标准库中的auto_ptr有的。重要的是对这些操作给出一个资源转移的语法,但是这也不是很难。 

   
template <class T>

SmartPointer<T>::SmartPointer (SmartPointer<T> & ptr)

{

_p = ptr.Release ();

} 

template <class T>

void SmartPointer<T>::operator = (SmartPointer<T> & ptr)

{

if (_p != ptr._p)

{

delete _p;

_p = ptr.Release ();

}

}

 

  使这整个想法迅速成功的原因之一是我可以以值方式传递这种封装指针!我有了我的蛋糕,并且也可以吃了。看这个Stack的新的实现: 

class Stack

{

enum { maxStack = 3 };

public:

Stack ()

: _top (0)

{}

void Push (SmartPointer<Item> & item) throw (char *)

{

if (_top >= maxStack)

throw "Stack overflow";

_arr [_top++] = item;

}

SmartPointer<Item> Pop ()

{

if (_top == 0)

return SmartPointer<Item> ();

return _arr [--_top];

}

private

int _top;

SmartPointer<Item> _arr [maxStack];

};

 

  Pop方法强制客户将其返回值赋给一个Strong Pointer,SmartPointer<Item> 。任何试图将他对一个普通指针的赋值都会产生一个编译期错误,因为类型不匹配。此外,因为Pop以值方式返回一个Strong Pointer( Pop的声明时SmartPointer<Item>后面没有 &符号),编译器在return 时自动进行了一个资源转换。他调用了operator =来从数组中提取一个Item, 拷贝构造函数将他传递给调用者。调用者最后拥有了指向 Pop赋值的Strong Pointer指向的一个 Item 

   我马上意识到我已经在某些东西之上了。我开始用了新的方法重写原来的代码。

1.2.5 Parser

   我过去有一个老的算术操作分析器,是用老的资源管理的技术写的。分析器的作用是在分析树中生成节点,节点是动态分配的。例如分析器的Expression方法生成一个表达式节点。我没有时间用 Strong Pointer去重写这个分析器。我令Expression TermFactor方法以传值的方式将Strong Pointer 返回到 Node中。看下面的Expression方法的实现: 

   
SmartPointer<Node> Parser::Expression()

{

// Parse a term

SmartPointer<Node> pNode = Term ();

EToken token = _scanner.Token();

if ( token == tPlus || token == tMinus )

{

// Expr := Term { ('+' | '-') Term }

SmartPointer<MultiNode> pMultiNode = new SumNode (pNode);

do

{

_scanner.Accept();

SmartPointer<Node> pRight = Term ();

pMultiNode->AddChild (pRight, (token == tPlus));

token = _scanner.Token();

} while (token == tPlus || token == tMinus);

pNode = up_cast<Node, MultiNode> (pMultiNode);

}

// otherwise Expr := Term

return pNode; // by value!

}

 

  最开始,Term方法被调用。他传值返回一个指向NodeStrong Pointer并且立刻把它保存到我们自己的Strong Pointer,pNode中。如果下一个符号不是加号或者减号,我们就简单的把这个 SmartPointer以值返回,这样就释放了Node的所有权。另外一方面,如果下一个符号是加号或者减号,我们创建一个新的 SumMode并且立刻(直接传递)将它储存到MultiNode的一个 Strong Pointer中。这里,SumNode是从MultiMode 中继承而来的,而MulitNode是从Node继承而来的。原来的 Node的所有权转给了SumNode 

  只要是他们在被加号和减号分开的时候,我们就不断的创建terms,我们将这些term 转移到我们的MultiNode中,同时MultiNode得到了所有权。最后,我们将指向 MultiNodeStrong Pointer向上映射为指向ModeStrong Pointer,并且将他返回调用着。 

  我们需要对Strong Pointers进行显式的向上映射,即使指针是被隐式的封装。例如,一个MultiNode 是一个Node,但是相同的is-a关系在 SmartPointer<MultiNode>SmartPointer<Node>之间并不存在,因为它们是分离的类(模板实例)并不存在继承关系。 up-cast模板是像下面这样定义的: 

template<class To, class From>

inline SmartPointer<To> up_cast (SmartPointer<From> & from)

{

return SmartPointer<To> (from.Release ());

}

 

  如果你的编译器支持新加入标准的成员模板(member template)的话,你可以为SmartPointer<T> 定义一个新的构造函数用来从接受一个class U 

template <class T>

template <class U> SmartPointer<T>::SmartPointer (SPrt<U> & uptr)

: _p (uptr.Release ())

{}

 

  这里的这个花招是模板在U不是T的子类的时候就不会编译成功(换句话说,只在 U is-a T的时候才会编译)。这是因为uptr的缘故。 Release()方法返回一个指向U的指针,并被赋值为_p,一个指向 T的指针。所以如果U不是一个T 的话,赋值会导致一个编译时刻错误。 

std::auto_ptr
 
 

   后来我意识到在STL中的auto_ptr 模板,就是我的Strong Pointer。在那时候还有许多的实现差异(auto_ptr Release方法并不将内部的指针清零--你的编译器的库很可能用的就是这种陈旧的实现),但是最后在标准被广泛接受之前都被解决了。

1.2.6 Transfer Semantics

  目前为止,我们一直在讨论在C++程序中资源管理的方法。宗旨是将资源封装到一些轻量级的类中,并由类负责它们的释放。特别的是,所有用 new操作符分配的资源都会被储存并传递进Strong Pointer(标准库中的auto_ptr )的内部。 

  这里的关键词是传递(passing)。一个容器可以通过传值返回一个Strong Pointer 来安全的释放资源。容器的客户只能够通过提供一个相应的Strong Pointer来保存这个资源。任何一个将结果赋给一个""指针的做法都立即会被编译器发现。 

auto_ptr<Item> item = stack.Pop (); // ok

Item * p = stack.Pop (); // Error! Type mismatch.

 

  以传值方式被传递的对象有value semantics 或者称为 copy semantics Strong Pointers是以值方式传递的--但是我们能说它们有 copy semantics吗?不是这样的!它们所指向的对象肯定没有被拷贝过。事实上,传递过后,源auto_ptr不在访问原有的对象,并且目标 auto_ptr成为了对象的唯一拥有者(但是往往auto_ptr的旧的实现即使在释放后仍然保持着对对象的所有权)。自然而然的我们可以将这种新的行为称作 Transfer Semantics 

  拷贝构造函数(copy construcor)和赋值操作符定义了auto_ptrTransfer Semantics,它们用了非const auto_ptr引用作为它们的参数。 

auto_ptr (auto_ptr<T> & ptr);

auto_ptr & operator = (auto_ptr<T> & ptr);

 

  这是因为它们确实改变了他们的源--剥夺了对资源的所有权。 

   通过定义相应的拷贝构造函数和重载赋值操作符,你可以将Transfer Semantics加入到许多对象中。例如,许多 Windows中的资源,比如动态建立的菜单或者位图,可以用有Transfer Semantics的类来封装。

1.2.7 Strong Vectors

  标准库只在auto_ptr中支持资源管理。甚至连最简单的容器也不支持ownership semantics 。你可能想将auto_ptr和标准容器组合到一起可能会管用,但是并不是这样的。例如,你可能会这样做,但是会发现你不能够用标准的方法来进行索引。 

vector< auto_ptr<Item> > autoVector;
 

  这种建造不会编译成功; 

Item * item = autoVector [0];
 

  另一方面,这会导致一个从autoVectauto_ptr 的所有权转换: 

auto_ptr<Item> item = autoVector [0];
 

  我们没有选择,只能够构造我们自己的Strong Vector。最小的接口应该如下:  

template <class T>

class auto_vector

{

public:

explicit auto_vector (size_t capacity = 0);

T const * operator [] (size_t i) const;

T * operator [] (size_t i);

void assign (size_t i, auto_ptr<T> & p);

void assign_direct (size_t i, T * p);

void push_back (auto_ptr<T> & p);

auto_ptr<T> pop_back ();

};

 

  你也许会发现一个非常防御性的设计态度。我决定不提供一个对vector的左值索引的访问,取而代之,如果你想设定 (set)一个值的话,你必须用assign或者assign_direct 方法。我的观点是,资源管理不应该被忽视,同时,也不应该在所有的地方滥用。在我的经验里,一个strong vector经常被许多push_back 方法充斥着。 

  Strong vector最好用一个动态的Strong Pointers 的数组来实现: 

template <class T>

class auto_vector

{

private

void grow (size_t reqCapacity); 

auto_ptr<T> *_arr;

size_t _capacity;

size_t _end;

};

 

  grow方法申请了一个很大的auto_ptr<T> 的数组,将所有的东西从老的书组类转移出来,在其中交换,并且删除原来的数组。 

  auto_vector的其他实现都是十分直接的,因为所有资源管理的复杂度都在auto_ptr 中。例如,assign方法简单的利用了重载的赋值操作符来删除原有的对象并转移资源到新的对象: 

void assign (size_t i, auto_ptr<T> & p)

{

_arr [i] = p;

}

 

  我已经讨论了push_backpop_back 方法。push_back方法传值返回一个auto_ptr,因为它将所有权从 auto_vector转换到auto_ptr中。 

  对auto_vector的索引访问是借助auto_ptrget方法来实现的,get简单的返回一个内部指针。 

T * operator [] (size_t i)

{

return _arr [i].get ();

}

 

  没有容器可以没有iterator。我们需要一个iteratorauto_vector看起来更像一个普通的指针向量。特别是,当我们废弃iterator的时候,我们需要的是一个指针而不是 auto_ptr。我们不希望一个auto_vector iterator 在无意中进行资源转换。 

template<class T>

class auto_iterator: public

iterator<random_access_iterator_tag, T *>

{

public:

auto_iterator () : _pp (0) {}

auto_iterator (auto_ptr<T> * pp) : _pp (pp) {}

bool operator != (auto_iterator<T> const & it) const

{ return it._pp != _pp; }

auto_iterator const & operator++ (int) { return _pp++; }

auto_iterator operator++ () { return ++_pp; }

T * operator * () { return _pp->get (); }

private

auto_ptr<T> * _pp;

};

   我们给auto_vect提供了标准的beginend方法来找回iterator 

   
class auto_vector

{

public:

typedef auto_iterator<T> iterator;

iterator begin () { return _arr; }

iterator end () { return _arr + _end; }

}; 

 

  你也许会问我们是否要利用资源管理重新实现每一个标准的容器?幸运的是,不;事实是strong vector 解决了大部分所有权的需求。当你把你的对象都安全的放置到一个strong vector中,你可以用所有其它的容器来重新安排(weak pointer 

   设想,例如,你需要对一些动态分配的对象排序的时候。你将它们的指针保存到一个strong vector中。然后你用一个标准的 vector来保存从strong vector中获得的weak指针。你可以用标准的算法对这个 vector进行排序。这种中介vector叫做 permutation vector。相似的,你也可以用标准的maps, priority queues, heaps, hash tables等等。

1.2.8 Code Inspection

  如果你严格遵照资源管理的条款,你就不会再资源泄露或者两次删除的地方遇到麻烦。你也降低了访问野指针的几率。同样的,遵循原有的规则,用delete删除用 new申请的德指针,不要两次删除一个指针。你也不会遇到麻烦。但是,那个是更好的注意呢?  

  这两个方法有一个很大的不同点。就是和寻找传统方法的bug相比,找到违反资源管理的规定要容易的多。后者仅需要一个代码检测或者一个运行测试,而前者则在代码中隐藏得很深,并需要很深的检查。  

  设想你要做一段传统的代码的内存泄露检查。第一件事,你要做的就是grep所有在代码中出现的new ,你需要找出被分配空间地指针都作了什么。你需要确定导致删除这个指针的所有的执行路径。你需要检查break语句,过程返回,异常。原有的指针可能赋给另一个指针,你对这个指针也要做相同的事。 

  相比之下,对于一段用资源管理技术实现的代码。你也用grep检查所有的new ,但是这次你只需要检查邻近的调用: 

  ● 这是一个直接的Strong Pointer 转换,还是我们在一个构造函数的函数体中? 

  ● 调用的返回知是否立即保存到对象中,构造函数中是否有可以产生异常的代码。? 

  ● 如果这样的话析构函数中时候有delete? 

  下一步,你需要用grep查找所有的release 方法,并实施相同的检查。 

  不同点是需要检查、理解单个执行路径和只需要做一些本地的检验。这难道不是提醒你非结构化的和结构化的程序设计的不同吗?原理上,你可以认为你可以应付goto,并且跟踪所有的可能分支。另一方面,你可以将你的怀疑本地化为一段代码。本地化在两种情况下都是关键所在。  

  在资源管理中的错误模式也比较容易调试。最常见的bug是试图访问一个释放过的strong pointer 。这将导致一个错误,并且很容易跟踪。

1.2.9 共享的所有权

  为每一个程序中的资源都找出或者指定一个所有者是一件很容易的事情吗?答案是出乎意料的,是!如果你发现了一些问题,这可能说明你的设计上存在问题。还有另一种情况就是共享所有权是最好的甚至是唯一的选择。 

  共享的责任分配给被共享的对象和它的客户(client)。一个共享资源必须为它的所有者保持一个引用计数。另一方面,所有者再释放资源的时候必须通报共享对象。最后一个释放资源的需要在最后负责 free的工作。 

  最简单的共享的实现是共享对象继承引用计数的类RefCounted 

class RefCounted

{

public:

RefCounted () : _count (1) {}

int GetRefCount () const { return _count; }

void IncRefCount () { _count++; }

int DecRefCount () { return --_count; }

private

int _count;

};

 

  按照资源管理,一个引用计数是一种资源。如果你遵守它,你需要释放它。当你意识到这一事实的时候,剩下的就变得简单了。简单的遵循规则--再构造函数中获得引用计数,在析构函数中释放。甚至有一个 RefCountedsmart pointer等价物: 

template <class T>

class RefPtr

{

public:

RefPtr (T * p) : _p (p) {}

RefPtr (RefPtr<T> & p)

{

_p = p._p;

_p->IncRefCount ();

}

~RefPtr ()

{

if (_p->DecRefCount () == 0)

delete _p;

}

private

T * _p;

};

 

  注意模板中的T不比成为RefCounted 的后代,但是它必须有IncRefCountDecRefCount的方法。当然,一个便于使用的 RefPtr需要有一个重载的指针访问操作符。在RefPtr中加入转换语义学(transfer semantics )是读者的工作。

1.2.10 所有权网络

  链表是资源管理分析中的一个很有意思的例子。如果你选择表成为链(link)的所有者的话,你会陷入实现递归的所有权。每一个 link都是它的继承者的所有者,并且,相应的,余下的链表的所有者。下面是用smart pointer实现的一个表单元: 

class Link

{

// ...

private

auto_ptr<Link> _next;

};  

  最好的方法是,将连接控制封装到一个弄构进行资源转换的类中。 

  对于双链表呢?安全的做法是指明一个方向,如forward: 

class DoubleLink

{

// ...

private

DoubleLink *_prev;

auto_ptr<DoubleLink> _next;

};

 

  注意不要创建环形链表。 

  这给我们带来了另外一个有趣的问题--资源管理可以处理环形的所有权吗?它可以,用一个mark-and-sweep 的算法。这里是实现这种方法的一个例子: 

template<class T>

class CyclPtr

{

public:

CyclPtr (T * p)

:_p (p), _isBeingDeleted (false)

{}

~CyclPtr ()

{

_isBeingDeleted = true;

if (!_p->IsBeingDeleted ())

delete _p; 

} 

void Set (T * p)

{

_p = p;

}

bool IsBeingDeleted () const { return _isBeingDeleted; }

private

T * _p;

bool _isBeingDeleted;

};

 

  注意我们需要用class T来实现方法IsBeingDeleted ,就像从CyclPtr继承。对特殊的所有权网络普通化是十分直接的。 

  将原有代码转换为资源管理代码 

   如果你是一个经验丰富的程序员,你一定会知道找资源的bug是一件浪费时间的痛苦的经历。我不必说服你和你的团队花费一点时间来熟悉资源管理是十分值得的。你可以立即开始用这个方法,无论你是在开始一个新项目或者是在一个项目的中期。转换不必立即全部完成。下面是步骤。

    1. 首先,在你的工程中建立基本的Strong Pointer。然后通过查找代码中的new 来开始封装裸指针。
    2. 最先封装的是在过程中定义的临时指针。简单的将它们替换为auto_ptr并且删除相应的delete 。如果一个指针在过程中没有被删除而是被返回,用auto_ptr替换并在返回前调用release方法。在你做第二次传递的时候,你需要处理对 release的调用。注意,即使是在这点,你的代码也可能更加"精力充沛 "--你会移出代码中潜在的资源泄漏问题。
    3. 下面是指向资源的裸指针。确保它们被独立的封装到auto_ptr中,或者在构造函数中分配在析构函数中释放。如果你有传递所有权的行为的话,需要调用 release方法。如果你有容器所有对象,用Strong Pointers重新实现它们。
    4. 接下来,找到所有对release的方法调用并且尽力清除所有,如果一个release 调用返回一个指针,将它修改传值返回一个auto_ptr
    5. 重复着一过程,直到最后所有newrelease的调用都在构造函数或者资源转换的时候发生。这样,你在你的代码中处理了资源泄漏的问题。对其他资源进行相似的操作。
    6. 你会发现资源管理清除了许多错误和异常处理带来的复杂性。不仅仅你的代码会变得精力充沛,它也会变得简单并容易维护。

C++内存管理4

2 内存泄漏

2.1 C++中动态内存分配引发问题的解决方案

   假设我们要开发一个String类,它可以方便地处理字符串数据。我们可以在类中声明一个数组,考虑到有时候字符串极长,我们可以把数组大小设为 200,但一般的情况下又不需要这么多的空间,这样是浪费了内存。对了,我们可以使用new操作符,这样是十分灵活的,但在类中就会出现许多意想不到的问题,本文就是针对这一现象而写的。现在,我们先来开发一个 String类,但它是一个不完善的类。的确,我们要刻意地使它出现各种各样的问题,这样才好对症下药。好了,我们开始吧! 

    /* String.h */

    #ifndef STRING_H_

    #define STRING_H_ 

    class String

    {

    private:

    char * str; //存储数据

    int len; //字符串长度  

    public:

    String(const char * s); //构造函数

    String(); // 默认构造函数

    ~String(); // 析构函数

    friend ostream & operator<<(ostream & os,const String& st);

    };

    #endif 

    /*String.cpp*/ 

    #include iostream

    #include cstring

    #include "String.h"

    using namespace std;

    String::String(const char * s)

    {

    len = strlen(s);

    str = new char[len + 1];

    strcpy(str, s);  

    }//拷贝数据  

    String::String()

    {

    len =0;

    str = new char[len+1];

    str[0]='\0'; 

    } 

    String::~String()

    {

    cout<<"这个字符串将被删除: "<<str<<'\n';//为了方便观察结果,特留此行代码。

    delete [] str;

    } 

    ostream & operator<<(ostream & os, const String & st)

    {

    os << st.str;

    return os;

    } 

    /*test_right.cpp*/ 

    #include iostream

    #include stdlib.h

    #include "String.h"

    using namespace std;

    int main()

    {

    String temp("天极网");

    cout<<temp<< '\n';

    system("PAUSE");

    return 0;

    }

  

  运行结果:

  天极网 

  请按任意键继续. . .

 

  大家可以看到,以上程序十分正确,而且也是十分有用的。可是,我们不能被表面现象所迷惑!下面,请大家用test_String.cpp文件替换 test_right.cpp文件进行编译,看看结果。有的编译器可能就是根本不能进行编译! 

   test_String.cpp:

   
#include iostream

#include stdlib.h

#include "String.h" 

using namespace std; 

void show_right(const String&);

void show_String(const String);//注意,参数非引用,而是按值传递。  

int main()

{

String test1("第一个范例。");

String test2("第二个范例。");

String test3("第三个范例。");

String test4("第四个范例。");

cout<<"下面分别输入三个范例: \n";

cout<<test1<< endl;

cout<<test2<< endl;

cout<<test3<< endl;

String* String1=new String(test1);

cout<<*String1<< endl;

delete String1;

cout<<test1<< endl; //Dev-cpp上没有任何反应。

cout<<"使用正确的函数: "<<endl;

show_right(test2);

cout<<test2<< endl;

cout<<"使用错误的函数: "<<endl;

show_String(test2);

cout<<test2<< endl; //这一段代码出现严重的错误!

String String2(test3);

cout<<"String2: "<< String2<<endl;

String String3;

String3=test4;

cout<<"String3: "<< String3<<endl;

cout<<"下面,程序结束,析构函数将被调用。 "<<endl;

return 0;

}

void show_right(const String& a)

{

cout<<a<< endl;

}

void show_String(const String a)

{

cout<<a<< endl;

}

 

  运行结果:

  下面分别输入三个范例: 

  第一个范例。

  第二个范例。

  第三个范例。 

  第一个范例。 

  这个字符串将被删除:第一个范例。 

  使用正确的函数:

  

  第二个范例。

  第二个范例。 

  使用错误的函数:

  第二个范例。 

  这个字符串将被删除:第二个范例。 

  这个字符串将被删除:?=

  ?= 

  String2: 第三个范例。

  String3: 第四个范例。 

  下面,程序结束,析构函数将被调用。 

  这个字符串将被删除:第四个范例。 

  这个字符串将被删除:第三个范例。 

  这个字符串将被删除:?= 

  这个字符串将被删除:x = 

  这个字符串将被删除:?= 

  这个字符串将被删除:

 

   现在,请大家自己试试运行结果,或许会更加惨不忍睹呢!下面,我为大家一一分析原因。

   首先,大家要知道,C++类有以下这些极为重要的函数: 

   一:复制构造函数。 

   二:赋值函数。 

   我们先来讲复制构造函数。什么是复制构造函数呢?比如,我们可以写下这样的代码:String test1(test2);这是进行初始化。我们知道,初始化对象要用构造函数。可这儿呢?按理说,应该有声明为这样的构造函数: String(const String &);可是,我们并没有定义这个构造函数呀?答案是,C++提供了默认的复制构造函数,问题也就出在这儿。  

   1):什么时候会调用复制构造函数呢?(以String 类为例。) 

     在我们提供这样的代码:String test1(test2)时,它会被调用;当函数的参数列表为按值传递,也就是没有用引用和指针作为类型时,如: void show_String(const String),它会被调用。其实,还有一些情况,但在这儿就不列举了。 

   2):它是什么样的函数。 

   它的作用就是把两个类进行复制。拿String类为例,C ++提供的默认复制构造函数是这样的:

   
String(const String& a)

{

str=a.str;

len=a.len;

}

   在平时,这样并不会有任何的问题出现,但我们用了new操作符,涉及到了动态内存分配,我们就不得不谈谈浅复制和深复制了。以上的函数就是实行的浅复制,它只是复制了指针,而并没有复制指针指向的数据,可谓一点儿用也没有。打个比方吧!就像一个朋友让你把一个程序通过网络发给他,而你大大咧咧地把快捷方式发给了他,有什么用处呢?我们来具体谈谈:  

   假如,A对象中存储了这样的字符串:"C ++"。它的地址为2000。现在,我们把A对象赋给 B对象:String B=A。现在,A B对象的str指针均指向2000地址。看似可以使用,但如果 B对象的析构函数被调用时,则地址2000处的字符串" C ++"已经被从内存中抹去,而A对象仍然指向地址2000。这时,如果我们写下这样的代码: cout<<A<<endl; 或是等待程序结束,A对象的析构函数被调用时,A对象的数据能否显示出来呢?只会是乱码。而且,程序还会这样做:连续对地址 2000处使用两次delete操作符,这样的后果是十分严重的! 

   本例中,有这样的代码:

   
String* String1=new String(test1);

cout<<*String1<< endl;

delete String1;

 

  假设test1str指向的地址为 2000,Stringstr 指针同样指向地址2000,我们删除了2000处的数据,而 test1对象呢?已经被破坏了。大家从运行结果上可以看到,我们使用cout<<test1 时,一点反应也没有。而在test1的析构函数被调用时,显示是这样:"这个字符串将被删除:"。 

   再看看这段代码:

   
cout<<"使用错误的函数: "<<endl;

show_String(test2);

cout<<test2<< endl;//这一段代码出现严重的错误!

 

   show_String函数的参数列表void show_String(const String a)是按值传递的,所以,我们相当于执行了这样的代码: String a=test2;函数执行完毕,由于生存周期的缘故,对象a被析构函数删除,我们马上就可以看到错误的显示结果了:这个字符串将被删除: ?=。当然,test2也被破坏了。解决的办法很简单,当然是手工定义一个复制构造函数喽!人力可以胜天! 

String::String(const String& a) 

len=a.len; 
str=new char(len+1); 
strcpy(str,a.str); 
}
 

  我们执行的是深复制。这个函数的功能是这样的:假设对象A中的str 指针指向地址2000,内容为"I am a C++ Boy!"。我们执行代码 String B=A时,我们先开辟出一块内存,假设为3000。我们用strcpy 函数将地址2000的内容拷贝到地址3000中,再将对象 Bstr指针指向地址3000。这样,就互不干扰了。 

   大家把这个函数加入程序中,问题就解决了大半,但还没有完全解决,问题在赋值函数上。我们的程序中有这样的段代码:

   
String String3;

String3=test4;

 

  经过我前面的讲解,大家应该也会对这段代码进行寻根摸底:凭什么可以这样做:String3=test4???原因是, C++为了用户的方便,提供的这样的一个操作符重载函数:operator=。所以,我们可以这样做。大家应该猜得到,它同样是执行了浅复制,出了同样的毛病。比如,执行了这段代码后,析构函数开始大展神威 ^_^。由于这些变量是后进先出的,所以最后的String3变量先被删除:这个字符串将被删除:第四个范例。很正常。最后,删除到test4 的时候,问题来了:这个字符串将被删除:?=。原因我不用赘述了,只是这个赋值函数怎么写,还有一点儿学问呢!大家请看: 

   平时,我们可以写这样的代码:x=y=z。(均为整型变量。)而在类对象中,我们同样要这样,因为这很方便。而对象 A=B=C就是A.operator=(B.operator=(c))。而这个operator= 函数的参数列表应该是:const String& a,所以,大家不难推出,要实现这样的功能,返回值也要是String& ,这样才能实现ABC 。我们先来写写看: 

String& String::operator=(const String& a)

{

delete [] str;//先删除自身的数据

len=a.len;

str=new char[len+1];

strcpy(str,a.str);//此三行为进行拷贝

return *this;//返回自身的引用

}

 

   是不是这样就行了呢?我们假如写出了这种代码:A=A,那么大家看看,岂不是把A 对象的数据给删除了吗?这样可谓引发一系列的错误。所以,我们还要检查是否为自身赋值。只比较两对象的数据是不行了,因为两个对象的数据很有可能相同。我们应该比较地址。以下是完好的赋值函数: 

String& String::operator=(const String& a)

{

if(this==&a)

return *this;

delete [] str;

len=a.len;

str=new char[len+1];

strcpy(str,a.str);

return *this;

}

 

把这些代码加入程序,问题就完全解决,下面是运行结果: 

  下面分别输入三个范例: 

  第一个范例

  第二个范例

  第三个范例 

  第一个范例 

  这个字符串将被删除:第一个范例。 

  第一个范例 

   使用正确的函数: 

  第二个范例。 

  第二个范例。 

   使用错误的函数: 

  第二个范例。 

  这个字符串将被删除:第二个范例。 

  第二个范例。 

  String2: 第三个范例。

  String3: 第四个范例。 

  下面,程序结束,析构函数将被调用。 

  这个字符串将被删除:第四个范例。

  这个字符串将被删除:第三个范例。

  这个字符串将被删除:第四个范例。

  这个字符串将被删除:第三个范例。

  这个字符串将被删除:第二个范例。

  这个字符串将被删除:第一个范例。

2.2 如何对付内存泄漏?

   写出那些不会导致任何内存泄漏的代码。很明显,当你的代码中到处充满了new 操作、delete 操作和指针运算的话,你将会在某个地方搞晕了头,导致内存泄漏,指针引用错误,以及诸如此类的问题。这和你如何小心地对待内存分配工作其实完全没有关系:代码的复杂性最终总是会超过你能够付出的时间和努力。于是随后产生了一些成功的技巧,它们依赖于将内存分配(allocations )与重新分配(deallocation)工作隐藏在易于管理的类型之后。标准容器(standard containers)是一个优秀的例子。它们不是通过你而是自己为元素管理内存,从而避免了产生糟糕的结果。想象一下,没有 stringvector的帮助,写出这个:

   
#include<vector>

#include<string>

#include<iostream>

#include<algorithm> 

using namespace std; 

int main() // small program messing around with strings

{

 cout << "enter some whitespace-separated words:\n";

 vector<string> v;

 string s;

 while (cin>>s) v.push_back(s);

 sort(v.begin(),v.end());

 string cat;

 typedef vector<string>::const_iterator Iter;

 for (Iter p = v.begin(); p!=v.end(); ++p) cat += *p+"+";

 cout << cat << '\n';

}

 

  你有多少机会在第一次就得到正确的结果?你又怎么知道你没有导致内存泄漏呢? 

  注意,没有出现显式的内存管理,宏,造型,溢出检查,显式的长度限制,以及指针。通过使用函数对象和标准算法(standard algorithm),我可以避免使用指针——例如使用迭代子( iterator),不过对于一个这么小的程序来说有点小题大作了。 

  这些技巧并不完美,要系统化地使用它们也并不总是那么容易。但是,应用它们产生了惊人的差异,而且通过减少显式的内存分配与重新分配的次数,你甚至可以使余下的例子更加容易被跟踪。早在1981年,我就指出,通过将我必须显式地跟踪的对象的数量从几万个减少到几打,为了使程序正确运行而付出的努力从可怕的苦工,变成了应付一些可管理的对象,甚至更加简单了。  

  如果你的程序还没有包含将显式内存管理减少到最小限度的库,那么要让你程序完成和正确运行的话,最快的途径也许就是先建立一个这样的库。 

  模板和标准库实现了容器、资源句柄以及诸如此类的东西,更早的使用甚至在多年以前。异常的使用使之更加完善。 

  如果你实在不能将内存分配/重新分配的操作隐藏到你需要的对象中时,你可以使用资源句柄(resource handle ),以将内存泄漏的可能性降至最低。这里有个例子:我需要通过一个函数,在空闲内存中建立一个对象并返回它。这时候可能忘记释放这个对象。毕竟,我们不能说,仅仅关注当这个指针要被释放的时候,谁将负责去做。使用资源句柄,这里用了标准库中的auto_ptr ,使需要为之负责的地方变得明确了。 

#include<memory>

#include<iostream> 

using namespace std; 

struct S {

 S() { cout << "make an S\n"; }

 ~S() { cout << "destroy an S\n"; }

 S(const S&) { cout << "copy initialize an S\n"; }

 S& operator=(const S&) { cout << "copy assign an S\n"; }

}; 

S* f()

{

 return new S; // 谁该负责释放这个S

}; 

auto_ptr<S> g()

{

 return auto_ptr<S>(new S); // 显式传递负责释放这个S

} 

int main()

{

 cout << "start main\n";

 S* p = f();

 cout << "after f() before g()\n";

 // S* q = g(); // 将被编译器捕捉

 auto_ptr<S> q = g();

 cout << "exit main\n";

 // *p产生了内存泄漏

 // *q被自动释放

}

 

  在更一般的意义上考虑资源,而不仅仅是内存。 

   如果在你的环境中不能系统地应用这些技巧(例如,你必须使用别的地方的代码,或者你的程序的另一部分简直是原始人类(译注:原文是Neanderthals,尼安德特人,旧石器时代广泛分布在欧洲的猿人)写的,如此等等),那么注意使用一个内存泄漏检测器作为开发过程的一部分,或者插入一个垃圾收集器( garbage collector)。

C++内存管理5


2.3浅谈C/C++内存泄漏及其检测工具

  对于一个c/c++程序员来说,内存泄漏是一个常见的也是令人头疼的问题。已经有许多技术被研究出来以应对这个问题,比如 Smart PointerGarbage Collection等。Smart Pointer 技术比较成熟,STL中已经包含支持Smart Pointer class,但是它的使用似乎并不广泛,而且它也不能解决所有的问题;Garbage Collection技术在Java 中已经比较成熟,但是在c/c++领域的发展并不顺畅,虽然很早就有人思考在C++中也加入 GC的支持。现实世界就是这样的,作为一个c/c++程序员,内存泄漏是你心中永远的痛。不过好在现在有许多工具能够帮助我们验证内存泄漏的存在,找出发生问题的代码。

2.3.1 内存泄漏的定义

   一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。应用程序一般使用malloc realloc new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。以下这段小程序演示了堆内存发生泄漏的情形:

   
void MyFunction(int nSize)

{

 char* p= new char[nSize];

 if( !GetStringFrom( p, nSize ) ){

  MessageBox("Error");

  return;

 }

 …//using the string pointed by p;

 delete p;

}

 

  当函数GetStringFrom()返回零的时候,指针p 指向的内存就不会被释放。这是一种常见的发生内存泄漏的情形。程序在入口处分配内存,在出口处释放内存,但是c函数可以在任何地方退出,所以一旦有某个出口处没有释放应该释放的内存,就会发生内存泄漏。 

  广义的说,内存泄漏不仅仅包含堆内存的泄漏,还包含系统资源的泄漏(resource leak),比如核心态 HANDLEGDI ObjectSOCKET Interface等,从根本上说这些由操作系统分配的对象也消耗内存,如果这些对象发生泄漏最终也会导致内存的泄漏。而且,某些对象消耗的是核心态内存,这些对象严重泄漏时会导致整个操作系统不稳定。所以相比之下,系统资源的泄漏比堆内存的泄漏更为严重。 

   GDI Object的泄漏是一种常见的资源泄漏:

   
void CMyView::OnPaint( CDC* pDC )

{

 CBitmap bmp;

 CBitmap* pOldBmp;

 bmp.LoadBitmap(IDB_MYBMP);

 pOldBmp = pDC->SelectObject( &bmp );

 

 if( Something() ){

  return;

 }

 pDC->SelectObject( pOldBmp );

 return;

}

 

  当函数Something()返回非零的时候,程序在退出前没有把pOldBmp 选回pDC中,这会导致pOldBmp指向的 HBITMAP 对象发生泄漏。这个程序如果长时间的运行,可能会导致整个系统花屏。这种问题在Win9x下比较容易暴露出来,因为Win9x GDI堆比Win2k NT 的要小很多。

2.3.2 内存泄漏的发生方式

  以发生的方式来分类,内存泄漏可以分为4类: 

  1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。比如例二,如果 Something()函数一直返回True,那么pOldBmp指向的 HBITMAP对象总是发生泄漏。 

  2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。比如例二,如果Something() 函数只有在特定环境下才返回True,那么pOldBmp指向的 HBITMAP对象并不总是发生泄漏。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。 

   3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,但是因为这个类是一个 Singleton,所以内存泄漏只会发生一次。另一个例子:

   
char* g_lpszFileName = NULL; 

void SetFileName( const char* lpcszFileName )

{

 if( g_lpszFileName ){

  free( g_lpszFileName );

 }

 g_lpszFileName = strdup( lpcszFileName );

}

 

  如果程序在结束的时候没有释放g_lpszFileName指向的字符串,那么,即使多次调用SetFileName() ,总会有一块内存,而且仅有一块内存发生泄漏。 

   4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。举一个例子:

   
class Connection

{

 public:

  Connection( SOCKET s);

  ~Connection();

  …

 private:

  SOCKET _socket;

  …

}; 

class ConnectionManager

{

 public:

  ConnectionManager(){}

  ~ConnectionManager(){

   list::iterator it;

   for( it = _connlist.begin(); it != _connlist.end(); ++it ){

    delete *it ;

   }

   _connlist.clear();

  }

  void OnClientConnected( SOCKET s ){

   Connection* p = new Connection(s);

   _connlist.push_back(p);

  }

  void OnClientDisconnected( Connection* pconn ){

   _connlist.remove( pconn );

   delete pconn;

  }

 private:

  list _connlist;

};

 

  假设在ClientServer端断开后, Server并没有呼叫OnClientDisconnected()函数,那么代表那次连接的 Connection对象就不会被及时的删除(在Server程序退出的时候,所有Connection 对象会在ConnectionManager的析构函数里被删除)。当不断的有连接建立、断开时隐式内存泄漏就发生了。 

   从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。

2.3.3 检测内存泄漏

  检测内存泄漏的关键是要能截获住对分配内存和释放内存的函数的调用。截获住这两个函数,我们就能跟踪每一块内存的生命周期,比如,每当成功的分配一块内存后,就把它的指针加入一个全局的list中;每当释放一块内存,再把它的指针从 list中删除。这样,当程序结束的时候,list中剩余的指针就是指向那些没有被释放的内存。这里只是简单的描述了检测内存泄漏的基本原理,详细的算法可以参见 Steve Maguire <<Writing Solid Code>>  

  如果要检测堆内存的泄漏,那么需要截获住malloc/realloc/freenew/delete 就可以了(其实new/delete最终也是用malloc/free的,所以只要截获前面一组即可)。对于其他的泄漏,可以采用类似的方法,截获住相应的分配和释放函数。比如,要检测 BSTR的泄漏,就需要截获SysAllocString/SysFreeString;要检测 HMENU的泄漏,就需要截获CreateMenu/ DestroyMenu。(有的资源的分配函数有多个,释放函数只有一个,比如,SysAllocStringLen 也可以用来分配BSTR,这时就需要截获多个分配函数) 

  在Windows平台下,检测内存泄漏的工具常用的一般有三种,MS C-Runtime Library 内建的检测功能;外挂式的检测工具,诸如,PurifyBoundsChecker等;利用 Windows NT自带的Performance Monitor。这三种工具各有优缺点, MS C-Runtime Library虽然功能上较之外挂式的工具要弱,但是它是免费的; Performance Monitor虽然无法标示出发生问题的代码,但是它能检测出隐式的内存泄漏的存在,这是其他两类工具无能为力的地方。  

  以下我们详细讨论这三种检测工具:

2.3.3.1 VC下内存泄漏的检测方法

  用MFC开发的应用程序,在DEBUG版模式下编译后,都会自动加入内存泄漏的检测代码。在程序结束后,如果发生了内存泄漏,在 Debug窗口中会显示出所有发生泄漏的内存块的信息,以下两行显示了一块被泄漏的内存块的信息: 

E:\TestMemLeak\TestDlg.cpp(70) : {59} normal block at 0x00881710, 200 bytes long. 

Data: <abcdefghijklmnop> 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 

  第一行显示该内存块由TestDlg.cpp文件,第70 行代码分配,地址在0x00881710,大小为200字节, {59}是指调用内存分配函数的Request Order,关于它的详细信息可以参见MSDN_CrtSetBreakAlloc()的帮助。第二行显示该内存块前16 个字节的内容,尖括号内是以 ASCII方式显示,接着的是以16进制方式显示。 

  一般大家都误以为这些内存泄漏的检测功能是由MFC提供的,其实不然。MFC 只是封装和利用了MS C-Runtime LibraryDebug Function。非 MFC程序也可以利用MS C-Runtime LibraryDebug Function 加入内存泄漏的检测功能。MS C-Runtime Library在实现malloc/free strdup等函数时已经内建了内存泄漏的检测功能。 

   注意观察一下由MFC Application Wizard生成的项目,在每一个cpp 文件的头部都有这样一段宏定义:

   
#ifdef _DEBUG

#define new DEBUG_NEW

#undef THIS_FILE

static char THIS_FILE[] = __FILE__;

#endif

 

   有了这样的定义,在编译DEBUG版时,出现在这个cpp 文件中的所有new都被替换成DEBUG_NEW了。那么 DEBUG_NEW是什么呢?DEBUG_NEW也是一个宏,以下摘自afx.h1632

   
#define DEBUG_NEW new(THIS_FILE, __LINE__)
 

   所以如果有这样一行代码:

   
char* p = new char[200];
 

   经过宏替换就变成了:

   
char* p = new( THIS_FILE, __LINE__)char[200];
 

   根据C++的标准,对于以上的new 的使用方法,编译器会去找这样定义的 operator new

   
void* operator new(size_t, LPCSTR, int)
 

   我们在afxmem.cpp 63行找到了一个这样的operator new 的实现

   
void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine)

{

 return ::operator new(nSize, _NORMAL_BLOCK, lpszFileName, nLine);

} 

void* __cdecl operator new(size_t nSize, int nType, LPCSTR lpszFileName, int nLine)

{

 

 pResult = _malloc_dbg(nSize, nType, lpszFileName, nLine);

 if (pResult != NULL)

  return pResult;

 

}

 

  第二个operator new函数比较长,为了简单期间,我只摘录了部分。很显然最后的内存分配还是通过_malloc_dbg 函数实现的,这个函数属于MS C-Runtime Library Debug Function 。这个函数不但要求传入内存的大小,另外还有文件名和行号两个参数。文件名和行号就是用来记录此次分配是由哪一段代码造成的。如果这块内存在程序结束之前没有被释放,那么这些信息就会输出到Debug窗口里。 

  这里顺便提一下THIS_FILE__FILE__LINE____FILE____LINE__ 都是编译器定义的宏。当碰到__FILE__时,编译器会把__FILE__ 替换成一个字符串,这个字符串就是当前在编译的文件的路径名。当碰到__LINE__时,编译器会把__LINE__替换成一个数字,这个数字就是当前这行代码的行号。在 DEBUG_NEW的定义中没有直接使用__FILE__,而是用了 THIS_FILE,其目的是为了减小目标文件的大小。假设在某个 cpp文件中有100 处使用了new,如果直接使用__FILE__,那编译器会产生 100个常量字符串,这100个字符串都是飧?/SPAN>cpp 文件的路径名,显然十分冗余。如果使用 THIS_FILE,编译器只会产生一个常量字符串,那100 new的调用使用的都是指向常量字符串的指针。 

  再次观察一下由MFC Application Wizard生成的项目,我们会发现在cpp 文件中只对new做了映射,如果你在程序中直接使用malloc函数分配内存,调用 malloc的文件名和行号是不会被记录下来的。如果这块内存发生了泄漏,MS C-Runtime Library仍然能检测到,但是当输出这块内存块的信息,不会包含分配它的的文件名和行号。 

   要在非MFC程序中打开内存泄漏的检测功能非常容易,你只要在程序的入口处加入以下几行代码:

   
int tmpFlag = _CrtSetDbgFlag( _CRTDBG_REPORT_FLAG );  

tmpFlag |= _CRTDBG_LEAK_CHECK_DF; 

_CrtSetDbgFlag( tmpFlag );

 

  这样,在程序结束的时候,也就是winmainmaindllmain函数返回之后,如果还有内存块没有释放,它们的信息会被打印到Debug窗口里。 

   如果你试着创建了一个非MFC应用程序,而且在程序的入口处加入了以上代码,并且故意在程序中不释放某些内存块,你会在 Debug窗口里看到以下的信息:

   
{47} normal block at 0x00C91C90, 200 bytes long. 

Data: < > 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

 

  内存泄漏的确检测到了,但是和上面MFC程序的例子相比,缺少了文件名和行号。对于一个比较大的程序,没有这些信息,解决问题将变得十分困难。 

  为了能够知道泄漏的内存块是在哪里分配的,你需要实现类似MFC的映射功能,把newmaolloc等函数映射到_malloc_dbg 函数上。这里我不再赘述,你可以参考 MFC的源代码。 

  由于Debug Function实现在MS C-RuntimeLibrary 中,所以它只能检测到堆内存的泄漏,而且只限于mallocrealloc strdup等分配的内存,而那些系统资源,比如HANDLEGDI Object ,或是不通过C-Runtime Library分配的内存,比如VARIANT BSTR的泄漏,它是无法检测到的,这是这种检测法的一个重大的局限性。另外,为了能记录内存块是在哪里分配的,源代码必须相应的配合,这在调试一些老的程序非常麻烦,毕竟修改源代码不是一件省心的事,这是这种检测法的另一个局限性。 

   对于开发一个大型的程序,MS C-Runtime Library提供的检测功能是远远不够的。接下来我们就看看外挂式的检测工具。我用的比较多的是 BoundsChecker,一则因为它的功能比较全面,更重要的是它的稳定性。这类工具如果不稳定,反而会忙里添乱。到底是出自鼎鼎大名的NuMega,我用下来基本上没有什么大问题。

2.3.3.2 使用 BoundsChecker 检测内存泄漏

  BoundsChecker采用一种被称为 Code Injection 的技术,来截获对分配内存和释放内存的函数的调用。简单地说,当你的程序开始运行时,BoundsCheckerDLL 被自动载入进程的地址空间(这可以通过 system-levelHook实现),然后它会修改进程中对内存分配和释放的函数调用,让这些调用首先转入它的代码,然后再执行原来的代码。 BoundsChecker在做这些动作的时,无须修改被调试程序的源代码或工程配置文件,这使得使用它非常的简便、直接。 

  这里我们以malloc函数为例,截获其他的函数方法与此类似。 

  需要被截获的函数可能在DLL中,也可能在程序的代码里。比如,如果静态连结C-Runtime Library ,那么malloc函数的代码会被连结到程序里。为了截获住对这类函数的调用,BoundsChecker 会动态修改这些函数的指令。 

   以下两段汇编代码,一段没有BoundsChecker介入,另一段则有BoundsChecker 的介入:

   
126: _CRTIMP void * __cdecl malloc (

127: size_t nSize

128: )

129: { 

00403C10 push ebp

00403C11 mov ebp,esp

130: return _nh_malloc_dbg(nSize, _newmode, _NORMAL_BLOCK, NULL, 0);

00403C13 push 0

00403C15 push 0

00403C17 push 1

00403C19 mov eax,[__newmode (0042376c)]

00403C1E push eax

00403C1F mov ecx,dword ptr [nSize]

00403C22 push ecx

00403C23 call _nh_malloc_dbg (00403c80)

00403C28 add esp,14h

131: }

 

以下这一段代码有BoundsChecker介入:

126: _CRTIMP void * __cdecl malloc (

127: size_t nSize

128: )

129: { 

00403C10 jmp 01F41EC8

00403C15 push 0

00403C17 push 1

00403C19 mov eax,[__newmode (0042376c)]

00403C1E push eax

00403C1F mov ecx,dword ptr [nSize]

00403C22 push ecx

00403C23 call _nh_malloc_dbg (00403c80)

00403C28 add esp,14h

131: }

 

  当BoundsChecker介入后,函数malloc 的前三条汇编指令被替换成一条jmp指令,原来的三条指令被搬到地址01F41EC8处了。当程序进入 malloc后先jmp01F41EC8,执行原来的三条指令,然后就是 BoundsChecker的天下了。大致上它会先记录函数的返回地址(函数的返回地址在stack上,所以很容易修改),然后把返回地址指向属于 BoundsChecker的代码,接着跳到malloc 函数原来的指令,也就是在 00403c15的地方。当malloc函数结束的时候,由于返回地址被修改,它会返回到BoundsChecker 的代码中,此时BoundsChecker会记录由malloc分配的内存的指针,然后再跳转到到原来的返回地址去。 

  如果内存分配/释放函数在DLL中, BoundsChecker则采用另一种方法来截获对这些函数的调用。BoundsChecker通过修改程序的 DLL Import Tabletable中的函数地址指向自己的地址,以达到截获的目的。 

   截获住这些分配和释放函数,BoundsChecker就能记录被分配的内存或资源的生命周期。接下来的问题是如何与源代码相关,也就是说当 BoundsChecker检测到内存泄漏,它如何报告这块内存块是哪段代码分配的。答案是调试信息(Debug Information)。当我们编译一个 Debug版的程序时,编译器会把源代码和二进制代码之间的对应关系记录下来,放到一个单独的文件里(.pdb)或者直接连结进目标程序,通过直接读取调试信息就能得到分配某块内存的源代码在哪个文件,哪一行上。使用 Code InjectionDebug Information ,使BoundsChecker 不但能记录呼叫分配函数的源代码的位置,而且还能记录分配时的Call Stack,以及Call Stack 上的函数的源代码位置。这在使用像 MFC这样的类库时非常有用,以下我用一个例子来说明:

   
void ShowXItemMenu()

{

 

 CMenu menu; 

 menu.CreatePopupMenu();

 //add menu items.

 menu.TrackPropupMenu();

 

} 

void ShowYItemMenu( )

{

 

 CMenu menu;

 menu.CreatePopupMenu();

 //add menu items.

 menu.TrackPropupMenu();

 menu.Detach();//this will cause HMENU leak

 

} 

BOOL CMenu::CreatePopupMenu()

{

 

 hMenu = CreatePopupMenu();

 

}

 

   当调用ShowYItemMenu()时,我们故意造成HMENU 的泄漏。但是,对于BoundsChecker来说被泄漏的HMENU是在 class CMenu::CreatePopupMenu()中分配的。假设的你的程序有许多地方使用了CMenuCreatePopupMenu() 函数,如CMenu::CreatePopupMenu()造成的,你依然无法确认问题的根结到底在哪里,在 ShowXItemMenu() 中还是在ShowYItemMenu()中,或者还有其它的地方也使用了CreatePopupMenu() ?有了Call Stack的信息,问题就容易了。BoundsChecker会如下报告泄漏的 HMENU的信息:

   
Function

File

Line 

CMenu::CreatePopupMenu

E:\8168\vc98\mfc\mfc\include\afxwin1.inl

1009 

ShowYItemMenu

E:\testmemleak\mytest.cpp

100

 

  这里省略了其他的函数调用 

  如此,我们很容易找到发生问题的函数是ShowYItemMenu()。当使用MFC 之类的类库编程时,大部分的API调用都被封装在类库的class里,有了 Call Stack信息,我们就可以非常容易的追踪到真正发生泄漏的代码。 

  记录Call Stack信息会使程序的运行变得非常慢,因此默认情况下BoundsChecker 不会记录Call Stack信息。可以按照以下的步骤打开记录Call Stack信息的选项开关:

  1. 打开菜单:BoundsChecker|Setting…

  2. Error Detection页中,在 Error Detection SchemeList中选择 Custom

  3. Category Combox中选择 Pointer and leak error check

  4. 钩上Report Call Stack 复选框

  5. 点击Ok 

  基于Code InjectionBoundsChecker 还提供了API Parameter的校验功能,memory over run等功能。这些功能对于程序的开发都非常有益。由于这些内容不属于本文的主题,所以不在此详述了。  

   尽管BoundsChecker的功能如此强大,但是面对隐式内存泄漏仍然显得苍白无力。所以接下来我们看看如何用 Performance Monitor检测内存泄漏。

2.3.3.3 使用 Performance Monitor 检测内存泄漏

  NT的内核在设计过程中已经加入了系统监视功能,比如CPU 的使用率,内存的使用情况,I/O操作的频繁度等都作为一个个Counter,应用程序可以通过读取这些 Counter了解整个系统的或者某个进程的运行状况。Performance Monitor就是这样一个应用程序。 

  为了检测内存泄漏,我们一般可以监视Process对象的Handle Count Virutal Bytes Working Set三个 CounterHandle Count记录了进程当前打开的HANDLE 的个数,监视这个Counter有助于我们发现程序是否有Handle泄漏; Virtual Bytes记录了该进程当前在虚地址空间上使用的虚拟内存的大小,NT的内存分配采用了两步走的方法,首先,在虚地址空间上保留一段空间,这时操作系统并没有分配物理内存,只是保留了一段地址。然后,再提交这段空间,这时操作系统才会分配物理内存。所以, Virtual Bytes一般总大于程序的Working Set。监视 Virutal Bytes可以帮助我们发现一些系统底层的问题; Working Set记录了操作系统为进程已提交的内存的总量,这个值和程序申请的内存总量存在密切的关系,如果程序存在内存的泄漏这个值会持续增加,但是 Virtual Bytes却是跳跃式增加的。 

  监视这些Counter可以让我们了解进程使用内存的情况,如果发生了泄漏,即使是隐式内存泄漏,这些Counter 的值也会持续增加。但是,我们知道有问题却不知道哪里有问题,所以一般使用Performance Monitor来验证是否有内存泄漏,而使用 BoundsChecker来找到和解决。 

  当 Performance Monitor显示有内存泄漏,而BoundsChecker 却无法检测到,这时有两种可能:第一种,发生了偶发性内存泄漏。这时你要确保使用Performance Monitor和使用BoundsChecker 时,程序的运行环境和操作方法是一致的。第二种,发生了隐式的内存泄漏。这时你要重新审查程序的设计,然后仔细研究Performance Monitor记录的 Counter 的值的变化图,分析其中的变化和程序运行逻辑的关系,找到一些可能的原因。这是一个痛苦的过程,充满了假设、猜想、验证、失败,

C++内存管理6

3 缓冲区溢出

3.1 缓冲区溢出的四种保护方法

   目前有四种基本的方法保护缓冲区免受缓冲区溢出的攻击和影响。

3.1.1 编写正确的代码

  编写正确的代码 是一件非常有意义但耗时的工作,特别像编写C 语言那种具有容易出错倾向的程序(如:字符串的零结尾),这种风格是由于追求性能而忽视正确性的传统引起的。尽管花了很长的时间使得人们知道了如何编写安全的程序组具有安全漏洞的程序依旧出现。因此人们开发了一些工具和技术来帮助经验不足的程序员编写安全正确的程序。

  最简单的方法就是用grep来搜索源代码中容易产生漏洞的库的调用,比如对strcpysprintf 的调用,这两个函数都没有检查输入参数的长度。事实上,各个版本 C的标准库均有这样的问题存在。为了寻找一些常见的诸如缓冲区溢出和操作系统竞争条件等漏洞,一些代码检查小组检查了很多的代码。然而依然有漏网之鱼存在。尽管采用了strcpy sprintf这些替代函数来防止缓冲区溢出的发生,但是由于编写代码的问题,仍旧会有这种情况发生。比如lprm程序就是最好的例子,虽然它通过了代码的安全检查 ,但仍然有缓冲区溢出的问题存在。

  为了对付这些问题,人们开发了一些高级的查错工具,如faultinjection等。这些工具的目的在于通过人为随机地产生一些缓冲区溢出来寻找代码的安全漏洞。还有一些静态分析工具用于侦测缓冲区溢出的存在。虽然这些工具可以帮助程序员开发更安全的程序,但是由于 C语言的特点,这些工具不可能找出所有的缓冲区溢出漏洞。所以,侦错技术只能用来减少缓冲区溢出的可能,并不能完全地消除它的存在,除非程序员能保证他的程序万元一失。

3.1.2 非执行的缓冲区

   通过使被攻击程序的数据段地址空间不可执行,从而使得攻击者不可能执行被植入被攻击程序输入缓冲区的代码,这种技术被称为非执行的缓冲区技术。事实上,很多老的Unix系统都是这样设计的,但是近来的 UnixMS Windows系统为实现更好的性能和功能,往往在数据段中动态地放人可执行的代码。所以为了保持程序的兼容性不可能使得所有程序的数据段不可执行。但是我们可以设定堆栈数据段不可执行,这样就可以最大限度地保证了程序的兼容性。 LinuxSolaris都发布了有关这方面的内核补丁。因为几乎没有任何合的程序会在堆栈中存放代码,这种做法几乎不产生任何兼容性问题,除了在 Linux中的两个特例,这时可执行的代码必须被放入堆栈中: 

  1.信号传递

  Linux通过向进程堆栈释放代码然后引发中断来执行在堆栈中的代码进而实现向进程发送Unix 信号.非执行缓冲区的补丁在发送信号的时候是允许缓冲区可执行的. 

  2GCC的在线重用

  研究发现gcc在堆栈区里放置了可执行的代码以便在线重用。然而,关闭这个功能并不产生任何问题.只有部分功能似乎不能使用。非执行堆栈的保护可以有效地对付把代码植入自动变量的缓冲区溢出攻击,而对于其他形式的攻击则没有效果。通过引用一个驻留的程序的指针,就可以跳过这种保护措施。其他的攻击可以采用把代码植入堆或者静态数据段中来跳过保护。

3.1.3 数组边界检查

  植入代码引起缓冲区溢出是一个方面,扰乱程序的执行流程是另一个方面。不像非执行缓冲区保护,数组边界检查完全没有了缓冲区溢出的产生和攻击。这样,只要数组不能被溢出,溢出攻击也就无从谈起。为了实现数组边界检查,则所有的对数组的读写操作都应当被检查以确保对数组的操作在正确的范围内。最直接的方法是检查所有的数组操作,但是通常可以来用一些优化的技术来减少检查的次数。目前有以下的几种检查方法:  

  1Compaq C编译器

  Compaq公司为Alpha CPU开发的 C编译器支持有限度的边界检查(使用— check_bounds 参数)。这些限制是:只有显示的数组引用才被检查,比如"a[3]" 会被检查,而" *(a+3)"则不会。由于所有的C数组在传送的时候是指针传递的,所以传递给函数的的数组不会被检查。带有危险性的库函数如 strcpy不会在编译的时候进行边界检查,即便是指定了边界检查。在C语言中利用指针进行数组操作和传递是非常频繁的,因此这种局限性是非常严重的。通常这种边界检查用来程序的查错,而且不能保证不发生缓冲区溢出的漏洞。  

 2Jones KellyC的数组边界检查

  Richard JonesPaul Kelly 开发了一个gcc的补丁,用来实现对C程序完全的数组边界检查。由于没有改变指针的含义,所以被编译的程序和其他的 gcc模块具有很好的兼容性。更进一步的是,他们由此从没有指针的表达式中导出了一个"基"指针,然后通过检查这个基指针来侦测表达式的结果是否在容许的范围之内。当然,这样付出的性能上的代价是巨大的:对于一个频繁使用指针的程序,如向量乘法,将由于指针的频繁使用而使速度慢 30 倍。这个编译器目前还很不成熟,一些复杂的程序(elm)还不能在这个上面编译、执行通过。然而在它的一个更新版本之下,它至少能编译执行 ssh软件的加密软件包,但其实现的性能要下降12倍。 

  3Purify:存储器存取检查

  PurifyC程序调试时查看存储器使用的工具而不是专用的安全工具。 Purify使用"目标代码插入" 技术来检查所有的存储器存取。通过用Purify连接工具连接,可执行代码在执行的时候带来的性能的损失要下降3—5 倍。 

  4、类型——安全语言

  所有的缓冲区溢出漏洞都源于C语言的类型安全。如果只有类型—安全的操作才可以被允许执行,这样就不可能出现对变量的强制操作。如果作为新手,可以推荐使用具有类型—安全的语言如 JAVAML 

  但是作为 Java执行平台的Java 虚拟机是 C程序.因此攻击JVM的一条途径是使 JVM 的缓冲区溢出。因此在系统中采用缓冲区溢出防卫技术来使用强制类型—安全的语言可以收到预想不到的效果。

3.1.4 程序指针完整性检查

  程序指针完整性检查和边界检查有略微的不同。与防止程序指针被改变不同,程序指针完整性检查在程序指针被引用之前检测到它的改变。因此,即便一个攻击者成功地改变程序的指针,由于系统事先检测到了指针的改变,因此这个指针将不会被使用。与数组边界检查相比,这种方法不能解决所有的缓冲区溢出问题;采用其他的缓冲区溢出方法就可以避免这种检测。但是这种方法在性能上有很大的优势,而且兼容性也很好。  

  l、手写的堆栈监测

  SnarskiiFreeBSD开发丁一套定制的能通过监测 cpu堆栈来确定缓冲区溢出的libc。这个应用完全用手工汇编写的,而且只保护 libc中的当前有效纪录函数.这个应用达到了设计要求,对于基于libc 库函数的攻击具有很好的防卫,但是不能防卫其它方式的攻击。 

  2、堆栈保护

  堆栈保护是一种提供程序指针完整性检查的编译器技术.通过检查函数活动纪录中的返回地址来实现。堆栈保护作为gcc 的一个小的补丁,在每个函数中,加入了函数建立和销毁的代码。加入的函数建立代码实际上在堆栈中函数返回地址后面加了一些附加的字节。而在函数返回时,首先检查这个附加的字节是否被改动过,如果发生过缓冲区溢出的攻击,那么这种攻击很容易在函数返回前被检测到。但是,如果攻击者预见到这些附加字节的存在,并且能在溢出过程中同样地制造他们.那么它就能成功地跳过堆栈保护的检测。通常.我们有如下两种方案对付这种欺骗:  

  1.终止符号

  利用在C语言中的终止符号如o(nullCRLF,—1(Eof) 等这些符号不能在常用的字符串函数中使用,因为这些函数一旦遇到这些终止符号,就结束函数过程了。 

  2.随机符号

  利用一个在函数调用时产生的一个32位的随机数来实现保密,使得攻击者不可能猜测到附加字节的内容. 而且,每次调用附加字节的内容都在改变,也无法预测。通过检查堆栈的完整性的堆栈保护法是从Synthetix方法演变来的。Synthetix 方法通过使用准不变量来确保特定变量的正确性。这些特定的变量的改变是程序实现能预知的,而且只能在满足一定的条件才能可以改变。这种变量我们称为准不变量。Synthetix开发了一些工具用来保护这些变量。攻击者通过缓冲区溢出而产生的改变可以被系统当做非法的动作。在某些极端的情况下,这些准不变量有可能被非法改变,这时需要堆栈保护来提供更完善的保护了。实验的数据表明,堆栈保护对于各种系统的缓冲区溢出攻击都有很好的保护作用.并能保持较好的兼容性和系统性能。分析表明,堆栈保护能有效抵御现在的和将来的基于堆栈的攻击。堆栈保护版本的 Red Hat Linux 5 1已经在各种系统上运行了多年,包括个人的笔记本电脑和工作组文件服务器。  

  3、指针保护

  在堆栈保护设计的时候,冲击堆栈构成了缓冲区溢出攻击的常见的一种形式。有人推测存在一种模板来构成这些攻击( 1996年的时候)。从此,很多简单的漏洞被发现,实施和补丁后,很多攻击者开始用更一般的方法实施缓冲区溢出攻击。指针保护是堆钱保护针对这种情况的一个推广。通过在所有的代码指针之后放置附加字节来检验指针在被调用之前的合法性,如果检验失败,会发出报警信号和退出程序的执行,就如同在堆栈保护中的行为一样。这种方案有两点需要注意:  

  (1)附加字节的定位

  附加字节的空间是在被保护的变量被分配的时候分配的,同时在被保护字节初始化过程中被初始化。这样就带来了问题:为了保持兼容性,我们不想改变被保护变量的大小,因此我们不能简单地在变量的结构定义中加入附加字。还有,对各种类型也有不同附加字节数目。 

  (2)查附加字节

  每次程序指针被引用的时候都要检查附加字节的完整性。这个也存在问题因为"从存取器读"在编译器中没有语义,编译器更关心指针的使用,而各种优化算法倾向于从存储器中读人变量.还有随着变量类型的不同,读入的方法也各自不同。到目前为止,只有很少—部分使用非指针变量的攻击能逃脱指针保护的检测。但是,可以通过在编译器上强制对某一变量加入附加字节来实现检测,这时需要程序员自己手工加入相应的保护了。

C++内存管理7

4 探讨C++内存回收

4.1 C++内存对象大会战

  如果一个人自称为程序高手,却对内存一无所知,那么我可以告诉你,他一定在吹牛。用CC++ 写程序,需要更多地关注内存,这不仅仅是因为内存的分配是否合理直接影响着程序的效率和性能,更为主要的是,当我们操作内存的时候一不小心就会出现问题,而且很多时候,这些问题都是不易发觉的,比如内存泄漏,比如悬挂指针。笔者今天在这里并不是要讨论如何避免这些问题,而是想从另外一个角度来认识C++ 内存对象。  

  我们知道,C++将内存划分为三个逻辑区域:堆、栈和静态存储区。既然如此,我称位于它们之中的对象分别为堆对象,栈对象以及静态对象。那么这些不同的内存对象有什么区别了?堆对象和栈对象各有什么优劣了?如何禁止创建堆对象或栈对象了?这些便是今天的主题。

4.1.1 基本概念

 

  先来看看栈。栈,一般用于存放局部变量或对象,如我们在函数定义中用类似下面语句声明的对象:

Type stack_object ; 
 

  stack_object便是一个栈对象,它的生命期是从定义点开始,当所在函数返回时,生命结束。 

  另外,几乎所有的临时对象都是栈对象。比如,下面的函数定义:

Type fun(Type object);
 

  这个函数至少产生两个临时对象,首先,参数是按值传递的,所以会调用拷贝构造函数生成一个临时对象object_copy1 ,在函数内部使用的不是使用的不是 object,而是object_copy1,自然,object_copy1 是一个栈对象,它在函数返回时被释放;还有这个函数是值返回的,在函数返回时,如果我们不考虑返回值优化(NRV),那么也会产生一个临时对象object_copy2 ,这个临时对象会在函数返回后一段时间内被释放。比如某个函数中有如下代码:

Type tt ,result ; //生成两个栈对象

tt = fun(tt); //函数返回时,生成的是一个临时对象object_copy2

 

  上面的第二个语句的执行情况是这样的,首先函数fun返回时生成一个临时对象object_copy2 ,然后再调用赋值运算符执行 

tt = object_copy2 ; //调用赋值运算符
 

  看到了吗?编译器在我们毫无知觉的情况下,为我们生成了这么多临时对象,而生成这些临时对象的时间和空间的开销可能是很大的,所以,你也许明白了,为什么对于"大"对象最好用const 引用传递代替按值进行函数参数传递了。  

  接下来,看看堆。堆,又叫自由存储区,它是在程序执行的过程中动态分配的,所以它最大的特性就是动态性。在C++中,所有堆对象的创建和销毁都要由程序员负责,所以,如果处理不好,就会发生内存问题。如果分配了堆对象,却忘记了释放,就会产生内存泄漏;而如果已释放了对象,却没有将相应的指针置为 NULL,该指针就是所谓的"悬挂指针",再度使用此指针时,就会出现非法访问,严重时就导致程序崩溃。 

  那么,C++中是怎样分配堆对象的?唯一的方法就是用new (当然,用类malloc指令也可获得C式堆内存),只要使用 new,就会在堆中分配一块内存,并且返回指向该堆对象的指针。 

  再来看看静态存储区。所有的静态对象、全局对象都于静态存储区分配。关于全局对象,是在main()函数执行前就分配好了的。其实,在 main()函数中的显示代码执行之前,会调用一个由编译器生成的_main()函数,而_main() 函数会进行所有全局对象的的构造及初始化工作。而在main()函数结束之前,会调用由编译器生成的exit函数,来释放所有的全局对象。比如下面的代码:

void mainvoid

{

 … …// 显式代码

}

 

  实际上,被转化成这样:

void mainvoid

{

 _main(); //隐式代码,由编译器产生,用以构造所有全局对象

 … … // 显式代码

 … …

 exit() ; // 隐式代码,由编译器产生,用以释放所有全局对象

}

 

  所以,知道了这个之后,便可以由此引出一些技巧,如,假设我们要在main()函数执行之前做某些准备工作,那么我们可以将这些准备工作写到一个自定义的全局对象的构造函数中,这样,在 main()函数的显式代码执行之前,这个全局对象的构造函数会被调用,执行预期的动作,这样就达到了我们的目的。 刚才讲的是静态存储区中的全局对象,那么,局部静态对象了?局部静态对象通常也是在函数中定义的,就像栈对象一样,只不过,其前面多了个 static关键字。局部静态对象的生命期是从其所在函数第一次被调用,更确切地说,是当第一次执行到该静态对象的声明代码时,产生该静态局部对象,直到整个程序结束时,才销毁该对象。 

  还有一种静态对象,那就是它作为class的静态成员。考虑这种情况时,就牵涉了一些较复杂的问题。 

  第一个问题是class的静态成员对象的生命期,class 的静态成员对象随着第一个class object的产生而产生,在整个程序结束时消亡。也就是有这样的情况存在,在程序中我们定义了一个class ,该类中有一个静态对象作为成员,但是在程序执行过程中,如果我们没有创建任何一个该class object,那么也就不会产生该class 所包含的那个静态对象。还有,如果创建了多个class object,那么所有这些object都共享那个静态对象成员。 

  第二个问题是,当出现下列情况时:

class Base

{

 public:

  static Type s_object ;

}

class Derived1 : public Base / / 公共继承

{

 … …// other data

}

class Derived2 : public Base / / 公共继承

{

 … …// other data

} 

Base example ;

Derivde1 example1 ;

Derivde2 example2 ;

example.s_object = …… ;

example1.s_object = …… ;

example2.s_object = …… ; 

 

  请注意上面标为黑体的三条语句,它们所访问的s_object是同一个对象吗?答案是肯定的,它们的确是指向同一个对象,这听起来不像是真的,是吗?但这是事实,你可以自己写段简单的代码验证一下。我要做的是来解释为什么会这样? 我们知道,当一个类比如Derived1,从另一个类比如 Base继承时,那么,可以看作一个Derived1对象中含有一个Base 型的对象,这就是一个subobject。一个Derived1对象的大致内存布局如下:

  

  让我们想想,当我们将一个Derived1型的对象传给一个接受非引用Base 型参数的函数时会发生切割,那么是怎么切割的呢?相信现在你已经知道了,那就是仅仅取出了Derived1型的对象中的subobject,而忽略了所有 Derived1自定义的其它数据成员,然后将这个subobject传递给函数(实际上,函数中使用的是这个 subobject的拷贝)。 

  所有继承Base类的派生类的对象都含有一个Base 型的subobject(这是能用Base型指针指向一个 Derived1对象的关键所在,自然也是多态的关键了),而所有的subobject和所有Base 型的对象都共用同一个s_object对象,自然,从Base类派生的整个继承体系中的类的实例都会共用同一个 s_object对象了。上面提到的exampleexample1example2的对象布局如下图所示:

4.1.2 三种内存对象的比较

  栈对象的优势是在适当的时候自动生成,又在适当的时候自动销毁,不需要程序员操心;而且栈对象的创建速度一般较堆对象快,因为分配堆对象时,会调用operator new操作, operator new会采用某种内存空间搜索算法,而该搜索过程可能是很费时间的,产生栈对象则没有这么麻烦,它仅仅需要移动栈顶指针就可以了。但是要注意的是,通常栈空间容量比较小,一般是1MB 2MB,所以体积比较大的对象不适合在栈中分配。特别要注意递归函数中最好不要使用栈对象,因为随着递归调用深度的增加,所需的栈空间也会线性增加,当所需栈空间不够时,便会导致栈溢出,这样就会产生运行时错误。 

  堆对象,其产生时刻和销毁时刻都要程序员精确定义,也就是说,程序员对堆对象的生命具有完全的控制权。我们常常需要这样的对象,比如,我们需要创建一个对象,能够被多个函数所访问,但是又不想使其成为全局的,那么这个时候创建一个堆对象无疑是良好的选择,然后在各个函数之间传递这个堆对象的指针,便可以实现对该对象的共享。另外,相比于栈空间,堆的容量要大得多。实际上,当物理内存不够时,如果这时还需要生成新的堆对象,通常不会产生运行时错误,而是系统会使用虚拟内存来扩展实际的物理内存。

接下来看看static对象。 

  首先是全局对象。全局对象为类间通信和函数间通信提供了一种最简单的方式,虽然这种方式并不优雅。一般而言,在完全的面向对象语言中,是不存在全局对象的,比如C#,因为全局对象意味着不安全和高耦合,在程序中过多地使用全局对象将大大降低程序的健壮性、稳定性、可维护性和可复用性。 C++也完全可以剔除全局对象,但是最终没有,我想原因之一是为了兼容C 

  其次是类的静态成员,上面已经提到,基类及其派生类的所有对象都共享这个静态成员对象,所以当需要在这些class之间或这些 class objects之间进行数据共享或通信时,这样的静态成员无疑是很好的选择。 

  接着是静态局部对象,主要可用于保存该对象所在函数被屡次调用期间的中间状态,其中一个最显著的例子就是递归函数,我们都知道递归函数是自己调用自己的函数,如果在递归函数中定义一个nonstatic局部对象,那么当递归次数相当大时,所产生的开销也是巨大的。这是因为 nonstatic局部对象是栈对象,每递归调用一次,就会产生一个这样的对象,每返回一次,就会释放这个对象,而且,这样的对象只局限于当前调用层,对于更深入的嵌套层和更浅露的外层,都是不可见的。每个层都有自己的局部对象和参数。 

  在递归函数设计中,可以使用static对象替代nonstatic 局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销,而且static对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。

4.1.3 使用栈对象的意外收获

 

  前面已经介绍到,栈对象是在适当的时候创建,然后在适当的时候自动释放的,也就是栈对象有自动管理功能。那么栈对象会在什么会自动释放了?第一,在其生命期结束的时候;第二,在其所在的函数发生异常的时候。你也许说,这些都很正常啊,没什么大不了的。是的,没什么大不了的。但是只要我们再深入一点点,也许就有意外的收获了。 

  栈对象,自动释放时,会调用它自己的析构函数。如果我们在栈对象中封装资源,而且在栈对象的析构函数中执行释放资源的动作,那么就会使资源泄漏的概率大大降低,因为栈对象可以自动的释放资源,即使在所在函数发生异常的时候。实际的过程是这样的:函数抛出异常时,会发生所谓的 stack_unwinding(堆栈回滚),即堆栈会展开,由于是栈对象,自然存在于栈中,所以在堆栈回滚的过程中,栈对象的析构函数会被执行,从而释放其所封装的资源。除非,除非在析构函数执行的过程中再次抛出异常――而这种可能性是很小的,所以用栈对象封装资源是比较安全的。基于此认识,我们就可以创建一个自己的句柄或代理来封装资源了。智能指针( auto_ptr)中就使用了这种技术。在有这种需要的时候,我们就希望我们的资源封装类只能在栈中创建,也就是要限制在堆中创建该资源封装类的实例。

4.1.4 禁止产生堆对象

  上面已经提到,你决定禁止产生某种类型的堆对象,这时你可以自己创建一个资源封装类,该类对象只能在栈中产生,这样就能在异常的情况下自动释放封装的资源。 

  那么怎样禁止产生堆对象了?我们已经知道,产生堆对象的唯一方法是使用new操作,如果我们禁止使用new 不就行了么。再进一步,new操作执行时会调用operator new,而 operator new是可以重载的。方法有了,就是使new operator private,为了对称,最好将operator delete也重载为private 。现在,你也许又有疑问了,难道创建栈对象不需要调用new吗?是的,不需要,因为创建栈对象不需要搜索内存,而是直接调整堆栈指针,将对象压栈,而 operator new的主要任务是搜索合适的堆内存,为堆对象分配空间,这在上面已经提到过了。好,让我们看看下面的示例代码:

#include <stdlib.h> //需要用到C式内存分配函数

class Resource ; //代表需要被封装的资源类

class NoHashObject

{

 private:

  Resource* ptr ;//指向被封装的资源

  ... ... //其它数据成员

  void* operator new(size_t size) //非严格实现,仅作示意之用

  {

   return malloc(size) ;

  }

  void operator delete(void* pp) //非严格实现,仅作示意之用

  {

   free(pp) ;

  }

 public:

  NoHashObject()

  {

   //此处可以获得需要封装的资源,并让ptr 指针指向该资源

   ptr = new Resource() ;

  }

  ~NoHashObject()

  {

   delete ptr ; //释放封装的资源

  }

};   

  NoHashObject现在就是一个禁止堆对象的类了,如果你写下如下代码: 

NoHashObject* fp = new NoHashObject() ; //编译期错误!

delete fp ; 

 

   上面代码会产生编译期错误。好了,现在你已经知道了如何设计一个禁止堆对象的类了,你也许和我一样有这样的疑问,难道在类NoHashObject的定义不能改变的情况下,就一定不能产生该类型的堆对象了吗?不,还是有办法的,我称之为"暴力破解法"。 C++是如此地强大,强大到你可以用它做你想做的任何事情。这里主要用到的是技巧是指针类型的强制转换。 

   
void main(void)

{

 char* temp = new char[sizeof(NoHashObject)] ;  

 //强制类型转换,现在ptr是一个指向 NoHashObject对象的指针

 NoHashObject* obj_ptr = (NoHashObject*)temp ;  

 temp = NULL ; //防止通过temp 指针修改NoHashObject对象  

 //再一次强制类型转换,让rp指针指向堆中 NoHashObject对象的ptr成员

 Resource* rp = (Resource*)obj_ptr ;  

 //初始化obj_ptr指向的 NoHashObject对象的ptr成员

 rp = new Resource() ;

 //现在可以通过使用obj_ptr指针使用堆中的 NoHashObject对象成员了

 ... ...  

 delete rp ;//释放资源

 temp = (char*)obj_ptr ;

 obj_ptr = NULL ;//防止悬挂指针产生

 delete [] temp ;//释放NoHashObject 对象所占的堆空间。

 

  上面的实现是麻烦的,而且这种实现方式几乎不会在实践中使用,但是我还是写出来路,因为理解它,对于我们理解C++内存对象是有好处的。对于上面的这么多强制类型转换,其最根本的是什么了?我们可以这样理解:  

  某块内存中的数据是不变的,而类型就是我们戴上的眼镜,当我们戴上一种眼镜后,我们就会用对应的类型来解释内存中的数据,这样不同的解释就得到了不同的信息。 

  所谓强制类型转换实际上就是换上另一副眼镜后再来看同样的那块内存数据。 

  另外要提醒的是,不同的编译器对对象的成员数据的布局安排可能是不一样的,比如,大多数编译器将NoHashObject ptr指针成员安排在对象空间的头4个字节,这样才会保证下面这条语句的转换动作像我们预期的那样执行:  

Resource* rp = (Resource*)obj_ptr ; 
 

  但是,并不一定所有的编译器都是如此。 

  既然我们可以禁止产生某种类型的堆对象,那么可以设计一个类,使之不能产生栈对象吗?当然可以。

4.1.5 禁止产生栈对象

 

  前面已经提到了,创建栈对象时会移动栈顶指针以"挪出"适当大小的空间,然后在这个空间上直接调用对应的构造函数以形成一个栈对象,而当函数返回时,会调用其析构函数释放这个对象,然后再调整栈顶指针收回那块栈内存。在这个过程中是不需要operator new/delete 操作的,所以将operator new/delete设置为private 不能达到目的。当然从上面的叙述中,你也许已经想到了:将构造函数或析构函数设为私有的,这样系统就不能调用构造/析构函数了,当然就不能在栈中生成对象了。 

  这样的确可以,而且我也打算采用这种方案。但是在此之前,有一点需要考虑清楚,那就是,如果我们将构造函数设置为私有,那么我们也就不能用 new来直接产生堆对象了,因为new在为对象分配空间后也会调用它的构造函数啊。所以,我打算只将析构函数设置为private 。再进一步,将析构函数设为private除了会限制栈对象生成外,还有其它影响吗?是的,这还会限制继承。 

  如果一个类不打算作为基类,通常采用的方案就是将其析构函数声明为private  

  为了限制栈对象,却不限制继承,我们可以将析构函数声明为protected,这样就两全其美了。如下代码所示: 

class NoStackObject

{

 protected:

  ~NoStackObject() { }

 public:

  void destroy()

  {

   delete this ;//调用保护析构函数

  }

}; 

 
 

  接着,可以像这样使用NoStackObject类: 

NoStackObject* hash_ptr = new NoStackObject() ;

... ... //hash_ptr指向的对象进行操作

hash_ptr->destroy() ; 

 

  呵呵,是不是觉得有点怪怪的,我们用new创建一个对象,却不是用delete 去删除它,而是要用destroy方法。很显然,用户是不习惯这种怪异的使用方式的。所以,我决定将构造函数也设为private protected。这又回到了上面曾试图避免的问题,即不用new,那么该用什么方式来生成一个对象了?我们可以用间接的办法完成,即让这个类提供一个 static成员函数专门用于产生该类型的堆对象。(设计模式中的singleton模式就可以用这种方式实现。)让我们来看看: 

class NoStackObject

{

 protected:

  NoStackObject() { }

  ~NoStackObject() { }

 public:

  static NoStackObject* creatInstance()

  {

   return new NoStackObject() ;//调用保护的构造函数

  }

  void destroy()

  {

   delete this ;//调用保护的析构函数

  }

};

 

  现在可以这样使用NoStackObject类了:  

NoStackObject* hash_ptr = NoStackObject::creatInstance() ;

... ... //hash_ptr指向的对象进行操作

hash_ptr->destroy() ;

hash_ptr = NULL ; //防止使用悬挂指针 

 

   现在感觉是不是好多了,生成对象和释放对象的操作一致了。

4.2 浅议C++ 中的垃圾回收方法

  许多 C 或者 C++ 程序员对垃圾回收嗤之以鼻,认为垃圾回收肯定比自己来管理动态内存要低效,而且在回收的时候一定会让程序停顿在那里,而如果自己控制内存管理的话,分配和释放时间都是稳定的,不会导致程序停顿。最后,很多 C/C++ 程序员坚信在C/C++ 中无法实现垃圾回收机制。这些错误的观点都是由于不了解垃圾回收的算法而臆想出来的。  

  其实垃圾回收机制并不慢,甚至比动态内存分配更高效。因为我们可以只分配不释放,那么分配内存的时候只需要从堆上一直的获得新的内存,移动堆顶的指针就够了;而释放的过程被省略了,自然也加快了速度。现代的垃圾回收算法已经发展了很多,增量收集算法已经可以让垃圾回收过程分段进行,避免打断程序的运行了。而传统的动态内存管理的算法同样有在适当的时间收集内存碎片的工作要做,并不比垃圾回收更有优势。  

  而垃圾回收的算法的基础通常基于扫描并标记当前可能被使用的所有内存块,从已经被分配的所有内存中把未标记的内存回收来做的。C/C++ 中无法实现垃圾回收的观点通常基于无法正确扫描出所有可能还会被使用的内存块,但是,看似不可能的事情实际上实现起来却并不复杂。首先,通过扫描内存的数据,指向堆上动态分配出来内存的指针是很容易被识别出来的,如果有识别错误,也只能是把一些不是指针的数据当成指针,而不会把指针当成非指针数据。这样,回收垃圾的过程只会漏回收掉而不会错误的把不应该回收的内存清理。其次,如果回溯所有内存块被引用的根,只可能存在于全局变量和当前的栈内,而全局变量 (包括函数内的静态变量)都是集中存在于 bss 段或 data段中。 

  垃圾回收的时候,只需要扫描 bss , data 段以及当前被使用着的栈空间,找到可能是动态内存指针的量,把引用到的内存递归扫描就可以得到当前正在使用的所有动态内存了。 

  如果肯为你的工程实现一个不错的垃圾回收器,提高内存管理的速度,甚至减少总的内存消耗都是可能的。如果有兴趣的话,可以搜索一下网上已有的关于垃圾回收的论文和实现了的库,开拓视野对一个程序员尤为重要。

14 novembre

水滴石穿C语言之C语言的底层操作

 
    概述
  C语言的内存模型基本上对应了现在von Neumann(冯·诺伊曼)计算机的实际存储模型,很好的达到了对机器的映射,这是C/C++适合做底层开发的主要原因,另外,C语言适合做底层开发还有另外一个原因,那就是C语言对底层操作做了很多的的支持,提供了很多比较底层的功能。

  下面结合问题分别进行阐述。

  问题:移位操作

  在运用移位操作符时,有两个问题必须要清楚:

  (1)、在右移操作中,腾空位是填 0 还是符号位;

  (2)、什么数可以作移位的位数。

  答案与分析:

  ">>"和"<<"是指将变量中的每一位向右或向左移动, 其通常形式为: 

  右移: 变量名>>移位的位数 

  左移: 变量名<<移位的位数 

  经过移位后, 一端的位被"挤掉",而另一端空出的位以0 填补,在C语言中的移位不是循环移动的。

  (1) 第一个问题的答案很简单,但要根据不同的情况而定。如果被移位的是无符号数,则填 0 。如果是有符号数,那么可能填 0 或符号位。如果你想解决右移操作中腾空位的填充问题,就把变量声明为无符号型,这样腾空位会被置 0。

  (2) 第二个问题的答案也很简单:如果移动 n 位,那么移位的位数要不小于 0 ,并且一定要小于 n 。这样就不会在一次操作中把所有数据都移走。

  比如,如果整型数据占 32 位,n 是一整型数据,则 n << 31 和 n << 0 都合法,而 n << 32 和 n << -1 都不合法。

  注意即使腾空位填符号位,有符号整数的右移也不相当与除以 。为了证明这一点,我们可以想一下 -1 >> 1 不可能为 0 。 

  问题:位段结构

struct RPR_ATD_TLV_HEADER
{
ULONG res1:6;
ULONG type:10;
ULONG res1:6;
ULONG length:10; 
}; 

  位段结构是一种特殊的结构, 在需按位访问一个字节或字的多个位时, 位结构比按位运算符更加方便。 

  位结构定义的一般形式为: 

struct位结构名{ 
 数据类型 变量名: 整型常数; 
 数据类型 变量名: 整型常数; 
} 位结构变量;  

  其中: 整型常数必须是非负的整数, 范围是0~15, 表示二进制位的个数, 即表示有多少位。

  变量名是选择项, 可以不命名, 这样规定是为了排列需要。 

  例如: 下面定义了一个位结构。 

struct{ 
 unsigned incon: 8; /*incon占用低字节的0~7共8位*/ 
 unsigned txcolor: 4;/*txcolor占用高字节的0~3位共4位*/ 
 unsigned bgcolor: 3;/*bgcolor占用高字节的4~6位共3位*/ 
 unsigned blink: 1; /*blink占用高字节的第7位*/ 
}ch;  

  位结构成员的访问与结构成员的访问相同。 

  例如: 访问上例位结构中的bgcolor成员可写成: 

ch.bgcolor  

  位结构成员可以与其它结构成员一起使用。 按位访问与设置,方便&节省

  例如: 

struct info{ 
 char name[8]; 
 int age; 
 struct addr address; 
 float pay; 
 unsigned state: 1; 
 unsigned pay: 1; 
}workers;'  

  上例的结构定义了关于一个工从的信息。其中有两个位结构成员, 每个位结构成员只有一位, 因此只占一个字节但保存了两个信息, 该字节中第一位表示工人的状态, 第二位表示工资是否已发放。由此可见使用位结构可以节省存贮空间。 

  注意不要超过值限制

  问题:字节对齐

  我在使用VC编程的过程中,有一次调用DLL中定义的结构时,发觉结构都乱掉了,完全不能读取正确的值,后来发现这是因为DLL和调用程序使用的字节对齐选项不同,那么我想问一下,字节对齐究竟是怎么一回事?

  答案与分析:

  关于字节对齐:

  1、 当不同的结构使用不同的字节对齐定义时,可能导致它们之间交互变得很困难。

  2、 在跨CPU进行通信时,可以使用字节对齐来保证唯一性,诸如通讯协议、写驱动程序时候寄存器的结构等。

  三种对齐方式:

  1、 自然对齐方式(Natural Alignment):与该数据类型的大小相等。

  2、 指定对齐方式 :

#pragma pack(8) //指定Align为 8;
#pragma pack() //恢复到原先值 

  3、 实际对齐方式:

Actual Align = min ( Order Align, Natual Align ) 

  对于复杂数据类型(比如结构等):实际对齐方式是其成员最大的实际对齐方式:

Actual Align = max( Actual align1,2,3,…) 

  编译器的填充规律:

  1、 成员为成员Actual Align的整数倍,在前面加Padding。

  成员Actual Align = min( 结构Actual Align,设定对齐方式)

  2、 结构为结构Actual Align的整数倍,在后面加Padding.

  例子分析:

#pragma pack(8) //指定Align为 8
struct STest1
{
char ch1; 
long lo1;
char ch2;
} test1;
#pragma pack() 

  现在

Align of STest1 = 4 , sizeof STest1 = 12 ( 4 * 3 ) 

  test1在内存中的排列如下( FF 为 padding ):

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 FF FF FF 01 01 01 01 01 FF FF FF 
ch1 -- lo1 -- ch2
#pragma pack(2) //指定Align为 2
struct STest2
{
char ch3;
STest1 test;
} test2;
#pragma pack() 

  现在 Align of STest1 = 2, Align of STest2 = 2 , sizeof STest2 = 14 ( 7 * 2 )

  test2在内存中的排列如下:

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
02 FF 01 FF FF FF 01 01 01 01 01 FF FF FF 
ch3 ch1 -- lo1 -- ch2 

  注意事项:

  1、 这样一来,编译器无法为特定平台做优化,如果效率非常重要,就尽量不要使用#pragma pack,如果必须使用,也最好仅在需要的地方进行设置。

  2、 需要加pack的地方一定要在定义结构的头文件中加,不要依赖命令行选项,因为如果很多人使用该头文件,并不是每个人都知道应该pack。这特别表现在为别人开发库文件时,如果一个库函数使用了struct作为其参数,当调用者与库文件开发者使用不同的pack时,就会造成错误,而且该类错误很不好查。

  3、 在VC及BC提供的头文件中,除了能正好对齐在四字节上的结构外,都加了pack,否则我们编的Windows程序哪一个也不会正常运行。

  4、 在 #pragma pack(n) 后一定不要include其他头文件,若包含的头文件中改变了align值,将产生非预期结果。

  5、 不要多人同时定义一个数据结构。这样可以保证一致的pack值。

  问题:按位运算符 

  C语言和其它高级语言不同的是它完全支持按位运算符。这与汇编语言的位操作有些相似。 C中按位运算符列出如下: 

━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 
操作符 作用 
──────────────────────────── 
& 位逻辑与 
| 位逻辑或 
^ 位逻辑异或 
- 位逻辑反 
>> 右移 
<< 左移 
━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 

  注意:

  1、 按位运算是对字节或字中的实际位进行检测、设置或移位, 它只适用于字符型和整数型变量以及它们的变体, 对其它数据类型不适用。 

  2、 关系运算和逻辑运算表达式的结果只能是1或0。 而按位运算的结果可以取0或1以外的值。 要注意区别按位运算符和逻辑运算符的不同, 例如, 若x=7, 则x&&8 的值为真(两个非零值相与仍为非零), 而x&8的值为0。 

  3、 | 与 ||,&与&&,~与! 的关系

  &、| 和 ~ 操作符把它们的操作数当作一个为序列,按位单独进行操作。比如:10 & 12 = 8,这是因为"&"操作符把 10 和 12 当作二进制描述 1010 和 1100 ,所以只有当两个操作数的相同位同时为 1 时,产生的结果中相应位才为 1 。同理,10 | 12 = 14 ( 1110 ),通过补码运算,~10 = -11 ( 11...110101 )。<以多少为一个位序列> &&、|| 和!操作符把它们的操作数当作"真"或"假",并且用 0 代表"假",任何非 0 值被认为是"真"。它们返回 1 代表"真",0 代表"假",对于"&&"和"||"操作符,如果左侧的操作数的值就可以决定表达式的值,它们根本就不去计算右侧的操作数。所以,!10 是 0 ,因为 10 非 0 ;10 && 12 是 1 ,因为 10 和 12 均非 0 ;10 || 12也是 1 ,因为 10 非 0 。并且,在最后一个表达式中,12 根本就没被计算,在表达式 10 || f( ) 中也是如此。




我为C狂

水滴石穿C语言之内存使用

2楼:孤鸟  发表时间:2006年1月20日 13:42  


问题:内存使用

  有人写了一个将整数转换为字符串的函数:


char *itoa (int n)
{
 char retbuf[20];
 sprintf(retbuf, "%d", n);
 return retbuf;



  如果我调用这个函数:char *str5 = itoa(5),str5会是什么结果呢?

  答案分析:

  答案是不确定,可以确定的是肯定不是我们想要的 "5"。

   retbuf定义在函数体中,是一个局部变量,它的内存空间位于栈(stack)中的某个位置,其作用范围也仅限于在itoa()这个函数中。当itoa()函数退出时,retbuf在调用栈中的内容将被收回,这时,这块内存地址可能存放别的内容。因此将retbuf这个局部变量返回给调用者是达不到预期的目的的。

  那么如何解决这个问题呢,不用担心,方法不但有,而且还不止一个,下面就来阐述三种能解决这个问题的办法:

  1)、在itoa()函数内部定义一个static char retbuf[20],根据静态变量的特性,我们知道,这可以保证函数返回后retbuf的空间不会被收回,原因是函数内的静态变量并不是放在栈中,而是放在程序中一个叫".bss"段的地方,这个地方的内容是不会因为函数退出而被收回的。

  这种办法确实能解决问题,但是这种办法同时也导致了itoa()函数变成了一个不可重入的函数(即不能保证相同的输入肯定有相同的输出),另外, retbuf [] 中的内容会被下一次的调用结果所替代,这种办法不值得推荐。

  2)、在itoa()函数内部用malloc() 为retbuf申请内存,并将结果存放其中,然后将retbuf返回给调用者。由于此时retbuf位于堆(heap)中,也不会随着函数返回而释放,因此可以达到我们的目的。

  但是有这样一种情况需要注意:itoa()函数的调用者在不需要retbuf的时候必须把它释放,否则就造成内存泄漏了,如果此函数和调用函数都是同一个人所写,问题不大,但如果不是,则比较容易会疏漏此释放内存的操作。

  3)、将函数定义为char *itoa(int n, char *retbuf),且retbuf的空间由调用者申请和释放,itoa()只是将转换结果存放到retbuf而已。

  这种办法明显比第一、二种方法要好,既避免了方法1对函数的影响,也避免了方法2对内存分配释放的影响,是目前一种比较通行的做法。

  扩展分析:

  其实就这个问题本身而言,我想大家都可以立刻想到答案,关键在于对内存这种敏感资源的正确和合理地利用,下面对内存做一个简单的分析:

  1)、程序中有不同的内存段,包括:

  .data - 已初始化全局/静态变量,在整个软件执行过程中有效;

  .bss - 未初始化全局/静态变量,在整个软件执行过程中有效;

  .stack - 函数调用栈,其中的内容在函数执行期间有效,并由编译器负责分配和收回;

  .heap - 堆,由程序显式分配和收回,如果不收回就是内存泄漏。

  2)、自己使用的内存最好还是自己申请和释放。

  这可以说是一个内存分配和释放的原则,比如说上面解决办法的第二种,由itoa()分配的内存,最后由调用者释放,就不是一个很好的办法,还不如用第三种,由调用者自己申请和释放。另外这个原则还有一层意思是说:如果你要使用一个指针,最好先确信它已经指向合法内存区了,如果没有就得自己分配,要不就是非法指针访问。很多程序的致命错误都是访问一个没有指向合法内存区的指针,这也包括空指针。

问题:内存分配 & sizeof 

  我使用sizeof来计算一个指针变量,我希望得到这个指针变量所分配的内存块的大小,可以吗?


Char *p = NULL;
int nMemSize = 0;

p = malloc(1024);
nMemSize = sizeof(p); 


  答案与分析: 

  答案是达不到你的要求,sizeof只能告诉你指针本身占用的内存大小。指针所指向的内存,如果是malloc分配的,sizeof 是没有办法知道的。换句话说,malloc分配的内存是没有办法向内存管理模块进行事后查询的,当然你可以自己编写代码来维护。

   问题:栈内存使用 

  下面程序运行有什么问题?


char *GetString(void)
{
 char p[] = "hello world";
 return p;// 编译器将提出警告
}

void Test4(void)
{
 char *str = NULL;
 str = GetString();// str 的内容是垃圾
 cout<< str << endl;



  答案与分析:

  返回栈内存,内存可能被销毁,也可能不被销毁,但是,出了作用域之后已被标记成可被系统使用,所以,乱七八糟不可知内容,当然,返回的指针的内容,应该是不变的,特殊时候是有用的,比如,可以用来探测系统内存分配规律等等。

  问题:内存使用相关编程规范 

  我想尽可能地避免内存使用上的问题,有什么捷径吗?

  答案与分析:

  除非做一件从没有人做过的事情,否则,都是有捷径可言的,那就是站在前人的肩膀上,现在各个大公司都有自己的编码规范,这些规范凝聚了很多的经验和教训,有较高的使用价值,鉴于这些规范在网上流传很多,这里我就不再列出了,感兴趣的,推荐参考林锐<<高质量C/C++编程指南>>

我为C狂

水滴石穿C语言之指针、数组和函数

3楼:孤鸟  发表时间:2006年1月20日 14:48  


水滴石穿C语言之指针、数组和函数 

    基本解释

  1、指针的本质是一个与地址相关的复合类型,它的值是数据存放的位置(地址);数组的本质则是一系列的变量。

  2、数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。指针可以随时指向任意类型的内存块,它的特征是"可变",所以我们常用指针来操作动态内存。

  3、当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。 

  问题:指针与数组

  听说char a[]与char *a是一致的,是不是这样呢?

  答案与分析:

  指针和数组存在着一些本质的区别。当然,在某种情况下,比如数组作为函数的参数进行传递时,由于该数组自动退化为同类型的指针,所以在函数内部,作为函数参数传递进来的指针与数组确实具有一定的一致性,但这只是一种比较特殊的情况而已,在本质上,两者是有区别的。请看以下的例子:

char a[] = "Hi, pig!";
char *p = "Hi, pig!"; 

  上述两个变量的内存布局分别如下:

  数组a需要在内存中占用8个字节的空间,这段内存区通过名字a来标志。指针p则需要4个字节的空间来存放地址,这4个字节用名字p来标志。其中存放的地址几乎可以指向任何地方,也可以哪里都不指,即空指针。目前这个p指向某地连续的8个字节,即字符串"Hi, pig!"。

  另外,例如:对于a[2]和p[2],二者都返回字符'i',但是编译器产生的执行代码却不一样。对于a[2],执行代码是从a的位置开始,向后移 动2两个字节,然后取出其中的字符。对于p[2],执行代码是从p的位置取出一个地址,在其上加2,然后取出对应内存中的字符。

  问题:数组指针

  为什么在有些时候我们需要定义指向数组而不是指向数组元素的指针?如何定义?

  答案与分析:

  使用指针,目的是用来保存某个元素的地址,从而来利用指针独有的优点,那么在元素需要是数组的情况下,就理所当然要用到指向数组的指针,比如在高维需要动态生成情况下的多维数组。

  定义例子如下: int (*pElement)[2]。

  下面是一个例子: 

int array[2][3] = {{1,2,3},{4,5,6}};
int (*pa)[3]; //定义一个指向数组的指针 
pa = &array[0]; // '&'符号能够体现pa的含义,表示是指向数组的指针
printf ("%d", (*pa)[0]); //将打印array[0][0],即1 
pa++; // 猜一猜,它指向谁?array[1]?对了!
printf ("%d", (*pa)[0]); // 将打印array[1][0],即4 

  上述这个例子充分说明了数组指针—一种指向整个数组的指针的定义和使用。

  需要说明的是,按照我们在第四篇讨论过的,指针的步进是参照其所指对象的大小的,因此,pa++将整个向后移 动一个数组的尺寸,而不是仅仅向后移 动一个数组元素的尺寸。

  问题:指针数组

  有如下定义:

struct UT_TEST_STRUCT *pTo[2][MAX_NUM]; 


  请分析这个定义的意义,并尝试说明这样的定义可能有哪些好处?

  答案与分析:

  前面我们谈了数组指针,现在又提到了指针数组,两者形式很相似,那么,如何区分两者的定义呢?分析如下:

  数组指针是:指向数组的指针,比如 int (*pA)[5]。

  指针数组是:指针构成的数组,比如int *pA[5]。

  至于上述指针数组的好处,大致有如下两个很普遍的原因:

  a)、各个指针内容可以按需要动态生成,避免了空间浪费。

  b)、各个指针呈数组形式排列,索引起来非常方便。

  在实际编程中,选择使用指针数组大多都是想要获得如上两个好处。


问题:指向指针的指针

  在做一个文本处理程序的时候,有这样一个问题:什么样的数据结构适合于按行存储文本?

  答案与分析:

  首先,我们来分析文本的特点,文本的主要特征是具有很强的动态性,一行文本的字符个数或多或少不确定,整个文本所拥有的文本行数也是不确定的。这样的特征决定了用固定的二维数组存放文本行必然限制多多,缺乏灵活性。这种场合,使用指向指针的指针有很大的优越性。

  现实中我们尝试用动态二维数组(本质就是指向指针的指针)来解决此问题:

  图示是一个指针数组。所谓动态性指横向(对应每行文本的字符个数)和纵向(对应整个文本的行数)两个方向都可以变化。

  就横向而言,因为指针的灵活性,它可以指向随意大小的字符数组,实现了横向动态性。

  就竖向而言,可以动态生成及扩展需要的指针数组的大小。

  下面的代码演示了这种动态数组的用途:

// 用于从文件中读取以 '\0'结尾的字符串的函数
extern char *getline(FILE *pFile);
FILE *pFile;
char **ppText = NULL; // 二维动态数组指针 
char *pCurrText = NULL; // 指向当前输入字符串的指针
ULONG ulCurrLines = 0;
ULONG ulAllocedLines = 0;

while (p = getline(pFile))
{
 if (ulCurrLines >= ulAllocedLines)
 {
  // * 当前竖向空间已经不够了,通过realloc对其进行扩展。
  ulAllocedLines += 50; // 每次扩展50行。 
  ppText = realloc (ppText, ulAllocedLines * (char *));
  if (NULL == ppText)
  {
   return; // 内存分配失败,返回 
  }
 }
 ppText[ulCurrLines++] = p; // 横向"扩展",指向不定长字符串 


  问题:指针数组与数组指针与指向指针的指针

  指针和数组分别有如下的特征:

  指针:动态分配,初始空间小

  数组:索引方便,初始空间大

  下面使用高维数组来说明指针数组、数组指针、指向指针的指针各自的适合场合。

   多维静态数组:各维均确定,适用于整体空间需求不大的场合,此结构可方便索引,例a[10][40]。

   数组指针:低维确定,高维需要动态生成的场合,例a[x][40]。

   指针数组:高维确定,低维需要动态生成的场合,例a[10][y]。

   指向指针的指针:高、低维均需要动态生成的场合,例a[x][y]。

  问题:数组名相关问题

  假设有一个整数数组a,a和&a的区别是什么?

  答案与分析:

  a == &a == &a[0],数组名a不占用存储空间。需要引用数组(非字符串)首地址的地方,我一般使用&a[0],使用a容易和指针混淆,使用&a容易和非指针变量混淆。

  区别在于二者的类型。对数组a的直接引用将产生一个指向数组第一个元素的指针,而&a的结果则产生一个指向全部数组的指针。例如:

int a[2] = {1, 2};
int *p = 0;
p = a; /* p指向a[0]所在的地方 */
x = *p; /* x = a[0] = 1*/
p = &a; /* 编译器会提示你错误,*/
/*显示整数指针与整数数组指针不一样 */ 

  问题:函数指针与指针函数
 
  请问:如下定义是什么意思:

int *pF1();
int (*pF2)();  

  答案与分析:

  首先清楚它们的定义:

   指针函数,返回一个指针的函数。

   函数指针,指向一个函数的指针。

  可知:

   pF1是一个指针函数,它返回一个指向int型数据的指针。

   pF2是一个函数指针,它指向一个参数为空的函数,这个函数返回一个整数。

 


我为C狂

水滴石穿C语言之可变参数问题

4楼:孤鸟  发表时间:2006年1月21日 11:53  


【简 介】
  C语言中有一种长度不确定的参数,形如:"…",它主要用在参数个数不确定的函数中,我们最容易想到的例子是printf函数。  

  原型: 

  int printf( const char *format [, argument]... ); 

  使用例: 

  printf("Enjoy yourself everyday!\n"); 

  printf("The value is %d!\n", value); 

  这种可变参数可以说是C语言一个比较难理解的部分,这里会由几个问题引发一些对它的分析。 

  注意:在C++中有函数重载(overload)可以用来区别不同函数参数的调用,但它还是不能表示任意数量的函数参数。 

  问题:printf的实现 

  请问,如何自己实现printf函数,如何处理其中的可变参数问题? 答案与分析: 

  在标准C语言中定义了一个头文件专门用来对付可变参数列表,它包含了一组宏,和一个va_list的typedef声明。一个典型实现如下: 

  typedef char* va_list; 

  #define va_start(list) list = (char*)&va_alist 

  #define va_end(list) 

  #define va_arg(list, mode)\ 

  ((mode*) (list += sizeof(mode)))[-1] 

  自己实现printf: 

  #include 

  int printf(char* format, …) 

  { 

  va_list ap; 

  va_start(ap, format); 

  int n = vprintf(format, ap); 

  va_end(ap); 

  return n; 

  } 

  问题:运行时才确定的参数 

  有没有办法写一个函数,这个函数参数的具体形式可以在运行时才确定? 

  答案与分析: 

  目前没有"正规"的解决办法,不过独门偏方倒是有一个,因为有一个函数已经给我们做出了这方面的榜样,那就是main(),它的原型是: 

  int main(int argc,char *argv[]); 
 
 函数的参数是argc和argv。 

  深入想一下,"只能在运行时确定参数形式",也就是说你没办法从声明中看到所接受的参数,也即是参数根本就没有固定的形式。常用的办法是你可以通过定义一个void *类型的参数,用它来指向实际的参数区,然后在函数中根据根据需要任意解释它们的含义。这就是main函数中argv的含义,而argc,则用来表明实际的参数个数,这为我们使用提供了进一步的方便,当然,这个参数不是必需的。 

  虽然参数没有固定形式,但我们必然要在函数中解析参数的意义,因此,理所当然会有一个要求,就是调用者和被调者之间要对参数区内容的格式,大小,有效性等所有方面达成一致,否则南辕北辙各说各话就惨了。 

  问题:可变长参数的传递 

  有时候,需要编写一个函数,将它的可变长参数直接传递给另外的函数,请问,这个要求能否实现? 

  答案与分析: 

  目前,你尚无办法直接做到这一点,但是我们可以迂回前进,首先,我们定义被调用函数的参数为va_list类型,同时在调用函数中将可变长参数列表转换为va_list,这样就可以进行变长参数的传递了。看如下所示: 

  void subfunc (char *fmt, va_list argp) 

  { 

  ... 

  arg = va_arg (fmt, argp); /* 从argp中逐一取出所要的参数 */ 

  ... 

  } 

  void mainfunc (char *fmt, ...) 

  { 

  va_list argp; 

  va_start (argp, fmt); /* 将可变长参数转换为va_list */ 

  subfunc (fmt, argp); /* 将va_list传递给子函数 */ 

  va_end (argp); 

  ... 

  } 

  问题:可变长参数中类型为函数指针 

  我想使用va_arg来提取出可变长参数中类型为函数指针的参数,结果却总是不正确,为什么? 

  答案与分析: 

  这个与va_arg的实现有关。一个简单的、演示版的va_arg实现如下: 

  #define va_arg(argp, type) \ 

  (*(type *)(((argp) += sizeof(type)) - sizeof(type))) 

  其中,argp的类型是char *。 

  如果你想用va_arg从可变参数列表中提取出函数指针类型的参数,例如 

  int (*)(),则va_arg(argp, int (*)())被扩展为: 

  (*(int (*)() *)(((argp) += sizeof (int (*)())) -sizeof (int (*)()))) 

  显然,(int (*)() *)是无意义的。 

  解决这个问题的办法是将函数指针用typedef定义成一个独立的数据类型,例如: 

  typedef int (*funcptr)(); 

  这时候再调用va_arg(argp, funcptr)将被扩展为: 

  (* (funcptr *)(((argp) += sizeof (funcptr)) - sizeof (funcptr))) 

  这样就可以通过编译检查了。 

  问题:可变长参数的获取 

  有这样一个具有可变长参数的函数,其中有下列代码用来获取类型为float的实参: 

  va_arg (argp, float); 

  这样做可以吗? 

  答案与分析: 

  不可以。在可变长参数中,应用的是"加宽"原则。也就是float类型被扩展成double;char, short被扩展成int。因此,如果你要去可变长参数列表中原来为float类型的参数,需要用va_arg(argp, double)。对char和short类型的则用va_arg(argp, int)。 

  问题:定义可变长参数的一个限制 

  为什么我的编译器不允许我定义如下的函数,也就是可变长参数,但是没有任何的固定参数? 

  int f (...) 

  { 

  ... 

  } 

  答案与分析: 

  不可以。这是ANSI C 所要求的,你至少得定义一个固定参数。 

  这个参数将被传递给va_start(),然后用va_arg()和va_end()来确定所有实际调用时可变长参数的类型和值。 

 
 


我为C狂

水滴石穿C语言之正确使用const

5楼:孤鸟  发表时间:2006年1月21日 12:21  


 水滴石穿C语言之正确使用const 
  
  基本解释

  const是一个C语言的关键字,它限定一个变量不允许被改变。使用const在一定程度上可以提高程序的健壮性,另外,在观看别人代码的时候,清晰理解const所起的作用,对理解对方的程序也有一些帮助。  虽然这听起来很简单,但实际上,const的使用也是c语言中一个比较微妙的地方,微妙在何处呢?请看下面几个问题。

  问题:const变量 & 常量

  为什么我象下面的例子一样用一个const变量来初始化数组,ANSI C的编译器会报告一个错误呢?

const int n = 5;
int a[n]; 

  答案与分析:

  1)、这个问题讨论的是"常量"与"只读变量"的区别。常量肯定是只读的,例如5, "abc",等,肯定是只读的,因为程序中根本没有地方存放它的值,当然也就不能够去修改它。而"只读变量"则是在内存中开辟一个地方来存放它的值,只不过这个值由编译器限定不允许被修改。C语言关键字const就是用来限定一个变量不允许被改变的修饰符(Qualifier)。上述代码中变量n被修饰为只读变量,可惜再怎么修饰也不是常量。而ANSI C规定数组定义时维度必须是"常量","只读变量"也是不可以的。

  2)、注意:在ANSI C中,这种写法是错误的,因为数组的大小应该是个常量,而const int n,n只是一个变量(常量 != 不可变的变量,但在标准C++中,这样定义的是一个常量,这种写法是对的),实际上,根据编译过程及内存分配来看,这种用法本来就应该是合理的,只是ANSI C对数组的规定限制了它。

  3)、那么,在ANSI C 语言中用什么来定义常量呢?答案是enum类型和#define宏,这两个都可以用来定义常量。 




  问题:const变量 & const 限定的内容

  下面的代码编译器会报一个错误,请问,哪一个语句是错误的呢?

typedef char * pStr;
char string[4] = "abc";
const char *p1 = string;
const pStr p2 = string;
p1++;
p2++;
  答案与分析:

  问题出在p2++上。

  1)、const使用的基本形式: const char m; 

  限定m不可变。

  2)、替换1式中的m, const char *pm; 

  限定*pm不可变,当然pm是可变的,因此问题中p1++是对的。

  3)、替换1式char, const newType m; 

  限定m不可变,问题中的charptr就是一种新类型,因此问题中p2不可变,p2++是错误的。

  问题:const变量 & 字符串常量

  请问下面的代码有什么问题?

char *p = "i'm hungry!";
p[0]= 'I'; 
  答案与分析:

  上面的代码可能会造成内存的非法写操作。分析如下, "i'm hungry"实质上是字符串常量,而常量往往被编译器放在只读的内存区,不可写。p初始指向这个只读的内存区,而p[0] = 'I'则企图去写这个地方,编译器当然不会答应。

  问题:const变量 & 字符串常量2

  请问char a[3] = "abc" 合法吗?使用它有什么隐患? 

  答案与分析:

  在标准C中这是合法的,但是它的生存环境非常狭小;它定义一个大小为3的数组,初始化为"abc",,注意,它没有通常的字符串终止符'

 
 
 


我为C狂

水滴石穿C语言之声明的语法

6楼:孤鸟  发表时间:2006年1月21日 16:12  


 
  概述 

  在很多情况下,尤其是读别人所写代码的时候,对C语言声明的理解能力变得非常重要,而C语言本身的凝练简约也使得C语言的声明常常会令人感到非常困惑,因此,在这里我用一篇的内容来集中阐述一下这个问题。

  问题:声明与函数 

  有一段程序存储在起始地址为0的一段内存上,如果我们想要调用这段程序,请问该如何去做? 

  答案

  答案是(*(void (*)( ) )0)( )。看起来确实令人头大,那好,让我们知难而上,从两个不同的途径来详细分析这个问题。 

  答案分析:从尾到头

  首先,最基本的函数声明:void function (paramList);

  最基本的函数调用:function(paramList);

  鉴于问题中的函数没有参数,函数调用可简化为 function();

  其次,根据问题描述,可以知道0是这个函数的入口地址,也就是说,0是一个函数的指针。使用函数指针的函数声明形式是:void (*pFunction)(),相应的调用形式是: (*pFunction)(),则问题中的函数调用可以写作:(*0)( )。

  第三,大家知道,函数指针变量不能是一个常数,因此上式中的0必须要被转化为函数指针。

  我们先来研究一下,对于使用函数指针的函数:比如void (*pFunction)( ),函数指针变量的原型是什么? 这个问题很简单,pFunction函数指针原型是( void (*)( ) ),即去掉变量名,清晰起见,整个加上()号。

  所以将0强制转换为一个返回值为void,参数为空的函数指针如下:( void (*)( ) )。

  OK,结合2)和3)的分析,结果出来了,那就是:(*(void (*)( ) )0)( ) 。 

  答案分析:从头到尾理解答案 

  (void (*)( )) ,是一个返回值为void,参数为空的函数指针原型。
  (void (*)( ))0,把0转变成一个返回值为void,参数为空的函数指针,指针指向的地址为0.
  *(void (*)( ))0,前面加上*表示整个是一个返回值为void的函数的名字
  (*(void (*)( ))0)( ),这当然就是一个函数了。

  我们可以使用typedef清晰声明如下:

  typedef void (*pFun)( );

  这样函数变为 (*(pFun)0 )( );

  问题:三个声明的分析 

  对声明进行分析,最根本的方法还是类比替换法,从那些最基本的声明上进行类比,简化,从而进行理解,下面通过分析三个例子,来具体阐述如何使用这种方法。

#1:int* (*a[5])(int, char*);

  首先看到标识符名a,"[]"优先级大于"*",a与"[5]"先结合。所以a是一个数组,这个数组有5个元素,每一个元素都是一个指针,指针指向"(int, char*)",很明显,指向的是一个函数,这个函数参数是"int, char*",返回值是"int*"。OK,结束了一个。:)

#2:void (*b[10]) (void (*)());

   b是一个数组,这个数组有10个元素,每一个元素都是一个指针,指针指向一个函数,函数参数是"void (*)()"【注10】,返回值是"void"。完毕!

  注意:这个参数又是一个指针,指向一个函数,函数参数为空,返回值是"void"。

#3. doube(*)() (*pa)[9];

   pa是一个指针,指针指向一个数组,这个数组有9个元素,每一个元素都是"doube(*)()"(也即一个函数指针,指向一个函数,这个函数的参数为空,返回值是"double")。  


我为C狂

水滴石穿C语言之代码检查工具

7楼:孤鸟  发表时间:2006年1月21日 16:28  


 
  概述

  PC-Lint是一个历史悠久,功能异常强劲的静态代码检测工具。它的使用历史可以追溯到计算机编程的远古时代(30多年以前)。经过这么多年的发展,它不但能够监测出许多语法逻辑上的隐患,而且也能够有效地帮你提出许多程序在空间利用、运行效率上的改进点,在很多专业级的软件公司,比如Microsoft, PC-Lint检查无错误无警告是代码首先要过的第一关,我个人觉得,对于小公司和个人开发而言,PC-Lint也非常重要,因为基于开发成本考虑,小公司和个人往往不能拿出很多很全面的测试,这时候,PC-Lint的强劲功能可以很好地提高软件的质量。

  功能 

  1) PC-Lint是一种静态代码检测工具,可以说,PC-LINT是一种更加严格的编译器,不仅可以象普通编译器那样检查出一般的语法错误,还可以检查出那些虽然完全合乎语法要求,但很可能是潜在的、不易发现的错误。

  2) PC-lint不但可以检测单个文件,也可以从整个项目的角度来检测问题,因为C语言编译器固有的单个编译,这些问题在编译器环境下很难被检测,而PC-Lint在检查当前文件的同时还会检查所有与之相关的文件,可想而知,它会对我们有很大的帮助。

  3) PC-lint支持几乎所有流行的编辑环境和编译器,比如Borland C++从1.x到5.x各个版本、Borland C++ Build、GCC、VC,VC.net、watcom C/C++、Source insight、intel C/C++等等,也支持16/32/64的平台环境。

  4) 支持Scott Meyes的名著(Effective C++/More Effective C++)中说描述的各种提高效率和防止错误的方法。

  四步速成PC-Lint概述

  这么好的工具,当然是先拿起来用了再说,这里分为四步,保证你马上可以使用PC-Lint了,当然,如果你上手以后,想细致深入学习这个工具,可以在网上查找相应的资料,另外在我的另一个系列文章《软件开发的七种武器》中有一篇是讲述 PC-Lint使用的,比这篇要深入细致一些,如有兴趣,也可以看看。 
  第一步:安装&设置

  安装和通常的软件没有什么不同。

  安装结束后在PC-Lint安装目录下运行CONFIG.exe,各项设置简要解释如下:

  a) 第一步,给出PC-Lint的安装路径和选择设置的保存文件。

  b) 第二步,选择你所使用的C/C++编译器:比如Visual C++ 6.x。

  c) 第三步,选择内存模式:比如32 bit Flat Model。

  d) 第四步,选择所使用的库:比如MFC,OWL,等,可多选,在windows下开发的一般要选中windows 32 bit。

  e) 第五步,C++编程提出过重要建议的作者,选择某作者后,他提出的编程建议方面的选项将被打开:比如Scott Meyers,可不选。

  f) 第六步,头文件的设置:可在后面步骤中手工添加。 

  第二步:整合PC-Lint到选定的编译环境

  当你在上述第二步中选择了编译器后,可以在PC-Lint目录下找到一个"env-选用的编辑环境.lnt"文件,比如对于Source insight  是env-si.lnt,对于vc6是env-vc6.lnt。

  打开此文件,根据上面的描述,配置你的编译器,下面以VC6为例:

  ·选取菜单 tools | customize..... 

  ·选取 Tools 标签

  ·点按主对话框上方的虚线小方框 New 

  ·输入 name: PC-LINT

  ·输入 Command: c:\\lint\\lint- nt.exe,假设安装到此路径

  ·输入 Arguments: c:\\lint\\std.lnt ?(FilePath)?7)选择 (x) Use Output Window

  ·Close

  ·完成后,在tools菜单下就会有一项PC-LINT选项。下面是TOOL配置图:

采集我?I0yUeEe

 

  第三步:Lint单个C文件

  a) 打开一个C文件

  b) 运行第2)步集成上的PC-Lint菜单项

  c) LINT结果会在一两秒之内出来。告警的解释可参考LINT安装目录下MSG.TXT文件,打开文件,通过查找告警号码定位,就可以看到比较详细的说明。

  第四步:Lint多个C文件

  a) 在LINT软件目录下建立PRJLINT.LNT文件,文件的第一行是使用的LINT配置文件名,从第二行开始,列出想所有要LINT的*.C文件名,一行一个。如果要把结果输出到文件,在前面加上一行"-os(文件名)"(例如:"-os(c:\\lint\\result.txt)")。样例:YF_Config.lnt

// 以下为自己想要进行Lint的.C文件,
G:\\src\\tt\\tt1.c
G:\\src\\tt\\tt2.c


  b) 用2)介绍的方法在Source Insight"中介绍的方法增加一条"PC-LINT PROJECT"命令,所有设置与"PC-LINT"设置相同,除了

  ①"Custom Commands"对话框中"RUN"编辑框的文字由"C:\\LINT\\LINT- NT.EXE filename.lnt %f"改为"C:\\LINT\\LINT-NT.EXE PRJLINT.LINT",也就是把配置文件名改为PRJLINT.LNT,并去掉未尾的"%f"

  ②设置的快捷键与PC-LINT要区别开来

  利用下面的DOS命令可以用来得到一堆*.C文件的清单。例如:

  ① 列出C:\\SRC下所有*.C文件,并保存到C:\\SRC\\FILELIST.TXT:

DIR C:\\SRC\\*.C /B >C:\\SRC\\FILELIST.TXT

  ② 列出C:\\SRC下,包括SRC的子目录下的所有的*.C文件,,并添加到C:\\SRC\\FILELIST.TXT的未尾:

DIR C:\\SRC\\*.C /S/B >>C:\\SRC\\FILELIST.TXT

  PC-Lint 重要文件说明

  Msg.txt  :解释告警的内容。

  选用的.lnt :包含头文件的路径,-i选项。

  env-选用的编辑环境.lnt :讲述如何将PC-lint与对应的编辑环境结合起来。

  co-xxx.lnt :选定的编译器。

  STD.LNT :内存模型等全局性东西。

  LIB-xxx.LNT :库类型的列表,包括标准C/C++库,MFC库,OWL库等等。

  AU-xxx.LNT  :C++编程提出过重要建议的作者,选择某作者后,他提出的编程建议方面的选项将被打开。

  OPTIONS.LNT :反映全局编译信息显示情况的选项文件。

 
 

链接图像

我为C狂

水滴石穿C语言之指针综合谈

8楼:孤鸟  发表时间:2006年1月21日 16:30  


 
  概述

  Joel Spolsky认为,对指针的理解是一种aptitude,不是通过训练就可以达到的。虽然如此,我还是想谈一谈这个C/C++语言中最强劲也是最容易出错的要素。

  鉴于指针和目前计算机内存结构的关联,很多C语言比较本质的特点都孕育在其中,因此,本篇和第六、第七两篇我都将以指针为主线,结合在实际编程中遇到的问题,来详细谈谈关于指针的几个重要方面。

  指针类型的本质分析

  1、指针的本质

  指针的本质:一种复合的数据类型。下面我将以下面几个作为例子进行展开分析:

  a)、int *p; 
  b)、int **p; 
  c)、int (*parValue)[3]; 
  d)、int (*pFun)(); 

  分析:

  所谓的数据类型就是具有某种数据特征的东东,比如数据类型char,它的数据特征就是它所占据的内存为1个字节, 指针也很类似,指针所指向的值也占据着内存中的一块地址,地址的长度与指针的类型有关,比如对于char型指针,这个指针占据的内存就是1个字节,因此指针也是一种数据类型,但我们知道指针本身也占据了一个内存空间地址,地址的长度和机器的字长有关,比如在32位机器中,这个长度就是4个字节,因此指针本身也同样是一种数据类型,因此,我们说,指针其实是一种复合的数据类型,

  好了,现在我们可以分析上面的几个例子了。

  假设有如下定义:

int nValue;  

  那么,nValue的类型就是int,也就是把nValue这个具体变量去掉后剩余的部分,因此,上面的4个声明可以类比进行分析:

  a)、int * 

  *代表变量(指针本身)的值是一个地址,int代表这个地址里面存放的是一个整数,这两个结合起来,int *定义了一个指向整数的指针,类推如下:

  b)、int ** 

  指向一个指向整数的指针的指针。

  c)、int (*)[3] 

  指向一个拥有三个整数的数组的指针。

  d)、int (*)() 

  指向一个函数的指针,这个函数参数为空,返回值为整数。

  分析结束,从上面可以看出,指针包括两个方面,一个是它本身的值,是一个内存中的地址;另一个是指针所指向的物,是这个地址中所存放着具有各种各样意义的数据。

  2、对指针本身值的分析

  下面例子考察指针本身的值(环境为32位的计算机):

void *p = malloc( 100 ); 

  请计算sizeof ( p ) = ?

char str[] = "Hello" ;
char *p = str ; 

  请计算sizeof ( p ) = ?

void Func ( char str[100])
{
请计算 sizeof( str ) = ? //注意,此时,str已经退化为一个指针,详情见
//下一篇指针与数组


  分析:上面的例子,答案都是4,因为从上面的讨论可以知道,指针本身的值对应着内存中的一个地址,它的size只与机器的字长有关(即它是由系统的内存模型决定的),在32位机器中,这个长度是4个字节。 

  3、对指针所指向物的分析

  现在再对指针这个复合类型的第二部分,指针所指向物的意义进行分析。

  上面我们已经得到了指针本身的类型,那么将指针本身的类型去掉 "*"号就可得到指针所指向物的类型,分别如下:

  a)、int 

  所指向物是一个整数。

  b)、int* 

  所指向物是一个指向整数的指针。

  c)、int ()[3] 

  ()为空,可以去掉,变为int [3],所指向物是一个拥有三个整数的数组。

  d)、int ()()

  第一个()为空,可以去掉,变为int (),所指向物是一个函数,这个函数的参数为空,返回值为整数。

  4、附加分析

  另外,关于指针本身大小的问题,在C++中与C有所不同,这里我也顺带谈一下。

  在C++中,对于指向对象成员的指针,它的大小不一定是4个字节,这主要是因为在引入多重虚拟继承以及虚拟函数的时候,有些附加的信息也需要通过这个指针进行传递,因此指向对象成员的指针会增大,不论是指向成员数据,还是成员函数都是如此,具体与编译器的实现有关,你可以编写个很小的C++程序去验证一下。另外,对一个类的静态成员(static member,可以是静态成员变量或者静态成员函数)来说,指向它的指针只是普通的函数指针,而不是一个指向类成员的指针,所以它的大小不会增加,仍旧是4个字节。
  指针运算符&和*

  "&和*",它们是一对相反的操作,'&'取得一个物的地址(也就是指针本身),'*'得到一个地址里放的物(指针所指向的物)。这个东西可以是值(对象)、函数、数组、类成员(class member)等等。

  参照上面的分析我们可以很好地理解&与*。

  使用指针的好处?

  关于指针的本质和基本的运算符我们讨论过了,在这里,我想再笼总地谈一谈使用指针的必要性和好处,为我们今后的使用和对后面篇章的理解做好铺垫。简而言之,指针有以下好处:

  1)、方便使用动态分配的数组。

  这个解释我放在本系列第六篇中进行讲解。

  2)、对于相同类型(甚至是相似类型)的多个变量进行通用访问。

  就是用一个指针变量不断在多个变量之间指来指去,从而使得非常应用起来非常灵活,不过,这招也比较危险,需要小心使用:因为出现错误的指针是编程中非常忌讳的事情。

  3)、变相改变一个函数的值传递特性。

  说白了,就是指针的传地址作用,将一个变量的地址作为参数传给函数,这样函数就可以修改那个变量了。

  4)、节省函数调用代价。

  我们可以将参数,尤其是大个的参数(例如结构,对象等),将他们地址作为参数传给函数,这样可以省去编译器为它们制作副本所带来的空间和时间上的开销。

  5)、动态扩展数据结构。

  因为指针可以动态地使用malloc/new生成堆上的内存,所以在需要动态扩展数据结构的时候,非常有用;比如对于树、链表、Hash表等,这几乎是必不可少的特性。

  6)、与目前计算机的内存模型相对应,可按照内存地址进行直接存取,这使得C非常适合于一些较底层的应用。

  这也是C/C++指针一个强大的优点,我会在后面讲述C语言的底层操作时,较详细地介绍这个优点的应用。

  7)、遍历数组。

  据个例子来说吧,当你需要对字符串数组进行操作时,想一想,你当然要用字符串指针在字符串上扫来扫去。

  …实在太多了,你可以慢慢来补充^_^。
  指针本身的相关问题

  1、问题:空指针的定义

  曾经看过有的.h文件将NULL定义为0L,为什么?

  答案与分析:

  这是一个关于空指针宏定义的问题。指针在C语言中是经常使用的,有时需要将一个指针置为空指针,例如在指针变量初始化的时候。
C语言中的空指针和Pascal或者Lisp语言中的NIL具有相同的地位。那如何定义空指针呢?下面的语句是正确的:

char *p1 = 0;
int *p2;
if (p != 0)
{
...
}
p2 = 0; 

  也就是说,在指针变量的初始化、赋值、比较操作中,0会被编译器理解为要将指针置为空指针。至于空指针的内部表示是否是0,则随不同的机器类型而定,不过通常都是0。但是在另外一些场合下,例如函数的参数原型是指针类型,函数调用时如果将0作为参数传入,编译器则不能将其理解为空指针。此时需要明确的类型转换,例如:

void func (char *p);
func ((char *)0); 

  一般情况下,0是可以放在代码中和指针关联使用的,但是有些程序员(数量还不少呦!也许就包括你在内)不喜欢0的直白,认为其不能表示作为指针的特殊含义,于是要定义一个宏NULL,来明确表示空指针常量。这也是对的,人家C语言标准就明确说:" NULL应该被定义为与实现相关的空指针常量"。但是将NULL定义成什么样的值呢?我想你一定见过好几种定义NULL的方法:

#define NULL 0
#define NULL (char *)0
#define NULL (void *)0 

  在我们使用的绝大多数计算系统上,例如PC,上述定义是能够工作的。然而,世界上还有很多其它种类的计算机,其CPU也不是Intel的。在某些系统上,指针和整数的大小和内部表示并不一致,甚至不同类型的指针的大小都不一致。为了避免这种可移植性问题,0L是一种最为安全的、最妥帖的定义方式。0L的含义是: "值为0的整数常量表达式"。这与C语言给出的空指针定义完全一致。因此,建议采用0L作为空指针常量NULL的值。

  其实 NULL定义值,和操作系统的的平台有关, 将一个指针定义为 NULL, 其用意是为了保护操作系统,因为通过指针可以访问任何一块地址, 但是,有些数据是不许一般用户访问的,比如操作系统的核心数据。 当我们通过一个空(NULL)的指针去方位数据时,系统会提示非法, 那么系统又是如何知道的呢?? 

  以windows2000系统为例, 该系统规定系统中每个进程的起始地址(0x00000000)开始的某个地址范围内是存放系统数据的,用户进程无法访问, 所以当用户用空指针(0)访问时,其实访问的就是0x00000000地址的系统数据,由于该地址数据是受系统保护的,所以系统会提示错误(指针访问非法)。

  这也就是说NULL值不一定要定义成0,起始只要定义在系统的保护范围的地址空间内,比如定义成(0x00000001, 0x00000002)都会起到相同的作用,但是为了考虑到移植性,普遍定义为0 。 

  2、问题:与指针相关的编程规则&规则分析 

  指针既然这么重要,而且容易出错,那么有没有方法可以很好地减少这些指针相关问题的出现呢?

  答案与分析:

  减少出错的根本是彻底理解指针。

  在方法上,遵循一定的编码规则可能是最立竿见影的方法了,下面我来阐述一下与指针相关的编程规则:

  1) 未使用的指针初始化为NULL 。

  2) 在给指针分配空间前、分配后均应作判断。

  3) 指针所指向的内容删除后也要清除指针本身。

  要牢记指针是一个复合的数据结构这个本质,所以我们不论初始化和清除都要同时兼顾指针本身(上述规则1,3)和指针所指向的内容(上述规则2,3)这两个方面。

  遵循这些规则可以有效地减少指针出错,我们来看下面的例子:

void Test(void)
{
 char *str = (char *) malloc(100);
 strcpy(str, "hello");
 free(str); 
 if(str != NULL)
 {
  strcpy(str, "world"); 
  printf(str);
 }


  请问运行Test函数会有什么样的结果?

  答:

  篡改动态内存区的内容,后果难以预料,非常危险。因为free(str);之后,str成为野指针,if(str != NULL)语句不起作用。

  如果我们牢记规则3,在free(str)后增加语句:

str = NULL; 

  那么,就可以防止这样的错误发生。


我为C狂

水滴石穿C语言之编译器引出的问题

9楼:孤鸟  发表时间:2006年1月21日 16:31  



  基本解释 
本节主要探讨C编译器下面两方面的特点所引发的一系列常见的编程问题。 
对C文件进行分别编译:
C程序通常由几个小程序(.c文件)组成,编译器将这几个小程序分别编译,然后通过链接程序将它们组合在一起形成一个目标代码。由于编译器每次只能编译一个文件,因此它不能立即检查需要几个源文件配合才能发现的错误。 
对函数的参数和返回值建立临时变量
C编译器会对函数的参数建立临时参数,也可能会对函数的返回值隐含传递一个指针。因为这些临时变量的隐含性存在,使得在某些情况下,特别是有指针存在的时候,会引发一系列的问题。 
C文件中所包含的头文件会和C语言一同编译

C语言中被包含的头文件是和.c文件一起编译的,头文件中的问题会反映到.c文件的编译中。 
  问题:C文件的分别编译

  我有一个数组a定义在f1.c中,但是我想在f2.c中计算它的元素个数,用sizeof可以达到这个目的吗?

   答案与分析:

  答案是否定的,你没有办法达到目的,本质原因是sizeof操作符只是在"编译时(compile time)"起作用, 而C语言的编译单位是每次单个.c文件进行编译(其它语言也都如此)。因此,sizeof可以确定同一个源文件中某个数组的大小,但是对于定义在另一个源文件中的数组它无能为力了,因为那已经是"运行时(run time)"才能确定的事情了。

  一件事情要想做,总会有办法的,下面提供有三种可选的办法来解决这个问题:

  1)、定义一个全局变量,让它记住数组的大小,在另外一个.c文件中我们通过访问这个全局变量来得到数组的大小信息( 好像有点小题大做得不偿失^_^)。

  2)、在某个.h文件中用宏定义数组的大小,例如#define ARRAY_SIZE 50,然后在两个源文件中都包含这个.h文件,通过直接访问ARRAY_SIZE来得到定义在不同.c文件中的数组的大小。

  3)、设置数组的最后一个元素为特殊值,例如0,-1,NULL等,然后我们通过遍历数组来寻找这个特殊的结尾元素,从而判断数组的长度(这个办法效率低,也是笨笨的)。

   问题:函数返回值隐含传递指针

  下面的代码可以正常工作,但是在程序结束时会有一个致命错误产生。究竟是什么原因呢?

struct list
{
 char *item;
 struct list *next;
}

main (argc, argv)
{
 ...


  答案与分析:

  原因很简单,稍微注意一点不难发现,在定义结构list的右花括弧后面加一个分号就可以解决这个问题:

struct list 
{
 char *item;
 struct list *next;
};//缺了这个分号可不行! 

  好了,问题是解决了,但,你知道这个错误究竟导致了什么致命问题吗?问题不是表面上那么简单的,OK,让我们来看看事情背后的真相。

  首先看一看下面这段代码:

VOID Func ( struct my_struct stX)
{
 .......
}
struct my_struct stY = {...};
Func (stY); 

  当调用函数Func的时候,是把结构变量stY的值拷贝一份到调用栈中,从而作为参数传递给函数FUNC的,这个叫做C语言的参数值传递。我相信这个你一定很清楚,那么,你应该知道:如果函数的返回值是结构变量的话,函数应该如何将值返回给调用者呢?且看下面这段代码:

struct my_structFunc (VOID)
{
 .......
}
struct my_struct stY = Func(); 

  此时函数Func的返回值是一个结构类型的值,这个返回值被放在内存中一个阴暗恐怖的地方,同时安排了一个指针指向这个地方(暂时称为"神秘指针"),而这个指针会由C语言的编译器作为一个隐藏参数传递给函数Func。当函数Func返回时,编译器生成的代码将这个由隐藏指针指向的内存区的值拷贝到返回结构stY中,从而完成将结构变量值返回给调用者。

  你明白了上述所讲的东东,那么今天问题的真正原因也就呼之欲出了:

  因为struct list {...}的定义后面没有加分号,导致主函数main (argc, argv)被编译器理解为是一个返回值为结构变量的函数,从而期望得到除了argc和argv以外的第三个参数,也就是我们上面提到的那个隐含传入的"神秘指针"。可是,大家知道,这里函数是main函数,main函数的参数是由程序中的启动代码(startup code)提供的。而启动代码当然认为main()天生就应该只得到两个参数,要"神秘指针",当然没有,如此一来, main()在返回时自作主张地去调用栈中访问它的那个并不存在的第三个参数(即神秘指针),这样导致非法访问,产生致命问题。这才是这个问题的真正根源。

  建议:

   1)、尽量将结构变量的指针而不是结构本身作为函数参数,否则函数调用时内存拷贝的开销可不小,尤其是对那些调用频繁、结构体大的情况。

   2)、结构定义的后面一定要加分号,经过上面我的大段讲述,我相信你不会犯相同的错误

  问题:编译器会给函数的参数隐含制造临时副本 

  请问运行下面的Test函数会有什么样的结果?

void GetMemory2(char **p, int num)
{
 *p = (char *)malloc(num);
}

void Test(void)
{
 char *str = NULL;
 GetMemory(&str, 100);
 strcpy(str, "hello");
 printf(str);


  答案与分析:

  这是林锐的《C/C++高质量编程指南》上面的例子,拿来用一下。

  这样调用会产生如下两个后果:

  1)、能够输出hello

  2)、内存泄漏

   另一个相关问题:

  请问运行Test函数会有什么样的结果?

void GetMemory(char *p)
{
 p = (char *)malloc(100);
}

void Test(void) 
{
 char *str = NULL;
 GetMemory(str);
 strcpy(str, "hello world");
 printf(str);


  答案与分析:

  后果严重,运行的结果是程序崩溃,通过运行调试我们可以看到,经过GetMemory后,Test函数中的 str仍旧是NULL。可想而知,一调用

strcpy(str, "hello world");  

  程序必然崩溃了事。

  原因分析:

   C编译器总是会为函数的每个参数制作临时副本,指针参数p的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西,如果想要输出动态内存,请使用指向指针的指针,或者,使用指向引用的指针。

  问题:头文件和包含它的.c文件一同编译问 

  下面的代码非常短小,看起来毫无问题,但编译器会报告一个错误,请问问题可能出现在什么地方?

#include "someheader.h"
int myint = 0; 

  答案与分析:

  不用盯着int myint = 0;看,这一句赋值应该是C语言中最简单的语句,问题肯定不会出在它身上, 那么问题只可能出现在someheader.h中,最常见的就是该头文件的最后一行的声明(函数也好,变量也好)没有用分号";"结尾,那么编译器会将它和myint变量结合起来考虑,自然就会出错了。

  这个问题主要是提醒你,在定位问题时思路要拓宽一点,可能要考虑一下所包含的头文件是否有问题。

  结论:被包含的头文件是和.c文件一起编译的,头文件中的问题会反映到.c文件编译中去的,切记。 




我为C狂

水滴石穿C语言之typedef的问题

10楼:孤鸟  发表时间:2006年1月21日 16:34  


 
  1. 基本解释

  typedef为C语言的关键字,作用是为一种数据类型定义一个新名字。这里的数据类型包括内部数据类型(int,char等)和自定义的数据类型(struct等)。

  在编程中使用typedef目的一般有两个,一个是给变量一个易记且意义明确的新名字,另一个是简化一些比较复杂的类型声明。

  至于typedef有什么微妙之处,请你接着看下面对几个问题的具体阐述。

 
 


  2. typedef & 结构的问题

  当用下面的代码定义一个结构时,编译器报了一个错误,为什么呢?莫非C语言不允许在结构中包含指向它自己的指针吗?请你先猜想一下,然后看下文说明:

typedef struct tagNode
{
 char *pItem;
 pNode pNext;
} *pNode;  

  答案与分析:

  1、typedef的最简单使用

typedef long byte_4; 

  给已知数据类型long起个新名字,叫byte_4。

  2、 typedef与结构结合使用

typedef struct tagMyStruct

 int iNum;
 long lLength;
} MyStruct; 

  这语句实际上完成两个操作:

  1) 定义一个新的结构类型

struct tagMyStruct

 int iNum; 
 long lLength; 
}; 

  分析:tagMyStruct称为"tag",即"标签",实际上是一个临时名字,struct 关键字和tagMyStruct一起,构成了这个结构类型,不论是否有typedef,这个结构都存在。

  我们可以用struct tagMyStruct varName来定义变量,但要注意,使用tagMyStruct varName来定义变量是不对的,因为struct 和tagMyStruct合在一起才能表示一个结构类型。

  2) typedef为这个新的结构起了一个名字,叫MyStruct。

typedef struct tagMyStruct MyStruct; 

  因此,MyStruct实际上相当于struct tagMyStruct,我们可以使用MyStruct varName来定义变量。

  答案与分析

  C语言当然允许在结构中包含指向它自己的指针,我们可以在建立链表等数据结构的实现上看到无数这样的例子,上述代码的根本问题在于typedef的应用。

  根据我们上面的阐述可以知道:新结构建立的过程中遇到了pNext域的声明,类型是pNode,要知道pNode表示的是类型的新名字,那么在类型本身还没有建立完成的时候,这个类型的新名字也还不存在,也就是说这个时候编译器根本不认识pNode。

  解决这个问题的方法有多种:

  1)、

typedef struct tagNode 
{
 char *pItem;
 struct tagNode *pNext;
} *pNode; 

  2)、

typedef struct tagNode *pNode;
struct tagNode 
{
 char *pItem;
 pNode pNext;
}; 

  注意:在这个例子中,你用typedef给一个还未完全声明的类型起新名字。C语言编译器支持这种做法。

  3)、规范做法:

struct tagNode
{
 char *pItem;
 struct tagNode *pNext;
};
typedef struct tagNode *pNode; 

  3. typedef & #define的问题

  有下面两种定义pStr数据类型的方法,两者有什么不同?哪一种更好一点?

typedef char *pStr;
#define pStr char *;  

  答案与分析:

  通常讲,typedef要比#define要好,特别是在有指针的场合。请看例子:

typedef char *pStr1;
#define pStr2 char *;
pStr1 s1, s2;
pStr2 s3, s4; 

  在上述的变量定义中,s1、s2、s3都被定义为char *,而s4则定义成了char,不是我们所预期的指针变量,根本原因就在于#define只是简单的字符串替换而typedef则是为一个类型起新名字。

  #define用法例子: 

#define f(x) x*x
main( )
{
 int a=6,b=2,c;
 c=f(a) / f(b);
 printf("%d \n",c);


  以下程序的输出结果是: 36。

  因为如此原因,在许多C语言编程规范中提到使用#define定义时,如果定义中包含表达式,必须使用括号,则上述定义应该如下定义才对:

#define f(x) (x*x) 

  当然,如果你使用typedef就没有这样的问题。

  4. typedef & #define的另一例

  下面的代码中编译器会报一个错误,你知道是哪个语句错了吗?

typedef char * pStr;
char string[4] = "abc";
const char *p1 = string;
const pStr p2 = string;
p1++;
p2++; 

  答案与分析:

  是p2++出错了。这个问题再一次提醒我们:typedef和#define不同,它不是简单的文本替换。上述代码中const pStr p2并不等于const char * p2。const pStr p2和const long x本质上没有区别,都是对变量进行只读限制,只不过此处变量p2的数据类型是我们自己定义的而不是系统固有类型而已。因此,const pStr p2的含义是:限定数据类型为char *的变量p2为只读,因此p2++错误。

(注:关于const的限定内容问题,在本系列第二篇有详细讲解)。

  #define与typedef引申谈

  1) #define宏定义有一个特别的长处:可以使用 #ifdef ,#ifndef等来进行逻辑判断,还可以使用#undef来取消定义。

  2) typedef也有一个特别的长处:它符合范围规则,使用typedef定义的变量类型其作用范围限制在所定义的函数或者文件内(取决于此变量定义的位置),而宏定义则没有这种特性。

  5. typedef & 复杂的变量声明

  在编程实践中,尤其是看别人代码的时候,常常会遇到比较复杂的变量声明,使用typedef作简化自有其价值,比如:

  下面是三个变量的声明,我想使用typdef分别给它们定义一个别名,请问该如何做?

>1:int *(*a[5])(int, char*);
>2:void (*b[10]) (void (*)());
>3. doube(*)() (*pa)[9]; 

  答案与分析:

  对复杂变量建立一个类型别名的方法很简单,你只要在传统的变量声明表达式里用类型名替代变量名,然后把关键字typedef加在该语句的开头就行了。 

  (注:如果你对有些变量的声明语法感到难以理解,请参阅本系列第十篇的相关内容)。

>1:int *(*a[5])(int, char*);
//pFun是我们建的一个类型别名
typedef int *(*pFun)(int, char*); 
//使用定义的新类型来声明对象,等价于int* (*a[5])(int, char*);
pFun a[5]; 

>2:void (*b[10]) (void (*)());
//首先为上面表达式蓝色部分声明一个新类型
typedef void (*pFunParam)();
//整体声明一个新类型
typedef void (*pFun)(pFunParam);
//使用定义的新类型来声明对象,等价于void (*b[10]) (void (*)());
pFun b[10];

>3. doube(*)() (*pa)[9]; 
//首先为上面表达式蓝色部分声明一个新类型
typedef double(*pFun)();
//整体声明一个新类型
typedef pFun (*pFunParam)[9];
//使用定义的新类型来声明对象,等价于doube(*)() (*pa)[9];
pFunParam pa;  

踏入C 中的雷区

                        ——C 内存管理详解
  伟大的Bill Gates 曾经失言:

  640K ought to be enough for everybody — Bill Gates 1981

  程序员们经常编写内存管理程序,往往提心吊胆。如果不想触雷,唯一的解决办法就是发现所有潜伏的地雷并且排除它们,躲是躲不了的。本文的内容比一般教科书的要深入得多,读者需细心阅读,做到真正地通晓内存管理。

  1、内存分配方式

  内存分配方式有三种:

  (1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。

  (2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

  (3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

  2、常见的内存错误及其对策

  发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。 常见的内存错误及其对策如下:

  * 内存分配未成功,却使用了它。

  编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行

  检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。

  * 内存分配虽然成功,但是尚未初始化就引用它。

  犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。 内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。

  * 内存分配成功并且已经初始化,但操作越过了内存的边界。

  例如在使用数组时经常发生下标"多1"或者"少1"的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。

  * 忘记了释放内存,造成内存泄露。

  含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。

  动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。

  * 释放了内存却继续使用它。
 
  有三种情况:

  (1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。

  (2)函数的return语句写错了,注意不要返回指向"栈内存"的"指针"或者"引用",因为该内存在函数体结束时被自动销毁。

  (3)使用free或delete释放了内存后,没有将指针设置为NULL。导致产生"野指针"。

  【规则1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。

  【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

  【规则3】避免数组或指针的下标越界,特别要当心发生"多1"或者"少1"操作。

  【规则4】动态内存的申请与释放必须配对,防止内存泄漏。

  【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生"野指针"。

  3、指针与数组的对比

  C /C程序中,指针和数组在不少地方可以相互替换着用,让人产生一种错觉,以为两者是等价的。

  数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。

  指针可以随时指向任意类型的内存块,它的特征是"可变",所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。

  下面以字符串为例比较指针与数组的特性。

  3.1 修改内容

  示例3-1中,字符数组a的容量是6个字符,其内容为hello。a的内容可以改变,如a[0]= 'X'。指针p指向常量字符串"world"(位于静态存储区,内容为world),常量字符串的内容是不可以被修改的。从语法上看,编译器并不觉得语句p[0]= 'X'有什么不妥,但是该语句企图修改常量字符串的内容而导致运行错误。

char a[] = "hello";
a[0] = 'X';
cout << a << endl;
char *p = "world"; // 注意p指向常量字符串
p[0] = 'X'; // 编译器不能发现该错误
cout << p << endl;
      示例3.1 修改数组和指针的内容

  3.2 内容复制与比较

  不能对数组名进行直接复制与比较。示例7-3-2中,若想把数组a的内容复制给数组b,不能用语句 b = a ,否则将产生编译错误。应该用标准库函数strcpy进行复制。同理,比较b和a的内容是否相同,不能用if(b==a) 来判断,应该用标准库函数strcmp进行比较。

  语句p = a 并不能把a的内容复制指针p,而是把a的地址赋给了p。要想复制a的内容,可以先用库函数malloc为p申请一块容量为strlen(a) 1个字符的内存,再用strcpy进行字符串复制。同理,语句if(p==a) 比较的不是内容而是地址,应该用库函数strcmp来比较。

// 数组…
char a[] = "hello";
char b[10];
strcpy(b, a); // 不能用 b = a;
if(strcmp(b, a) == 0) // 不能用 if (b == a)

// 指针…
int len = strlen(a);
char *p = (char *)malloc(sizeof(char)*(len 1));
strcpy(p,a); // 不要用 p = a;
if(strcmp(p, a) == 0) // 不要用 if (p == a)
       示例3.2 数组和指针的内容复制与比较

  3.3 计算内存容量

  用运算符sizeof可以计算出数组的容量(字节数)。示例7-3-3(a)中,sizeof(a)的值是12(注意别忘了'')。指针p指向a,但是sizeof(p)的值却是4。这是因为sizeof(p)得到的是一个指针变量的字节数,相当于sizeof(char*),而不是p所指的内存容量。C /C语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。

  注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。示例7-3-3(b)中,不论数组a的容量是多少,sizeof(a)始终等于sizeof(char *)。

char a[] = "hello world";
char *p = a;
cout<< sizeof(a) << endl; // 12字节
cout<< sizeof(p) << endl; // 4字节
     示例3.3(a) 计算数组和指针的内存容量

void Func(char a[100])
{
 cout<< sizeof(a) << endl; // 4字节而不是100字节
}
     示例3.3(b) 数组退化为指针


4、指针参数是如何传递内存的?

  如果函数的参数是一个指针,不要指望用该指针去申请动态内存。示例7-4-1中,Test函数的语句GetMemory(str, 200)并没有使str获得期望的内存,str依旧是NULL,为什么?

void GetMemory(char *p, int num)
{
 p = (char *)malloc(sizeof(char) * num);
}
void Test(void)
{
 char *str = NULL;
 GetMemory(str, 100); // str 仍然为 NULL
 strcpy(str, "hello"); // 运行错误
}
      示例4.1 试图用指针参数申请动态内存

  毛病出在函数GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。

  如果非得要用指针参数去申请内存,那么应该改用"指向指针的指针",见示例4.2。

void GetMemory2(char **p, int num)
{
 *p = (char *)malloc(sizeof(char) * num);
}
void Test2(void)
{
 char *str = NULL;
 GetMemory2(&str, 100); // 注意参数是 &str,而不是str
 strcpy(str, "hello");
 cout<< str << endl;
 free(str);
}
      示例4.2用指向指针的指针申请动态内存

  由于"指向指针的指针"这个概念不容易理解,我们可以用函数返回值来传递动态内存。这种方法更加简单,见示例4.3。

char *GetMemory3(int num)
{
 char *p = (char *)malloc(sizeof(char) * num);
 return p;
}
void Test3(void)
{
 char *str = NULL;
 str = GetMemory3(100);
 strcpy(str, "hello");
 cout<< str << endl;
 free(str);
}
       示例4.3 用函数返回值来传递动态内存

  用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把return语句用错了。这里强调不要用return语句返回指向"栈内存"的指针,因为该内存在函数结束时自动消亡,见示例4.4。

char *GetString(void)
{
 char p[] = "hello world";
 return p; // 编译器将提出警告
}
void Test4(void)
{
 char *str = NULL;
 str = GetString(); // str 的内容是垃圾
 cout<< str << endl;
}
      示例4.4 return语句返回指向"栈内存"的指针

  用调试器逐步跟踪Test4,发现执行str = GetString语句后str不再是NULL指针,但是str的内容不是"hello world"而是垃圾。
如果把示例4.4改写成示例4.5,会怎么样?

char *GetString2(void)
{
 char *p = "hello world";
 return p;
}
void Test5(void)
{
 char *str = NULL;
 str = GetString2();
 cout<< str << endl;
}
     示例4.5 return语句返回常量字符串

  函数Test5运行虽然不会出错,但是函数GetString2的设计概念却是错误的。因为GetString2内的"hello world"是常量字符串,位于静态存储区,它在程序生命期内恒定不变。无论什么时候调用GetString2,它返回的始终是同一个"只读"的内存块。

  5、杜绝"野指针"

  "野指针"不是NULL指针,是指向"垃圾"内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是"野指针"是很危险的,if语句对它不起作用。 "野指针"的成因主要有两种:

  (1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如

char *p = NULL;
char *str = (char *) malloc(100);

  (2)指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。

  (3)指针操作超越了变量的作用范围。这种情况让人防不胜防,示例程序如下:

class A
{
 public:
  void Func(void){ cout << "Func of class A" << endl; }
};
void Test(void)
{
 A *p;
 {
  A a;
  p = &a; // 注意 a 的生命期
 }
 p->Func(); // p是"野指针"
}

  函数Test在执行语句p->Func()时,对象a已经消失,而p是指向a的,所以p就成了"野指针"。但奇怪的是我运行这个程序时居然没有出错,这可能与编译器有关。


6、有了malloc/free为什么还要new/delete?

  malloc与free是C /C语言的标准库函数,new/delete是C 的运算符。它们都可用于申请动态内存和释放内存。

  对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。

   因此C 语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。我们先看一看malloc/free和new/delete如何实现对象的动态内存管理,见示例6。

class Obj
{
 public :
  Obj(void){ cout << "Initialization" << endl; }
  ~Obj(void){ cout << "Destroy" << endl; }
  void Initialize(void){ cout << "Initialization" << endl; }
  void Destroy(void){ cout << "Destroy" << endl; }
};
void UseMallocFree(void)
{
 Obj *a = (obj *)malloc(sizeof(obj)); // 申请动态内存
 a->Initialize(); // 初始化
 //…
 a->Destroy(); // 清除工作
 free(a); // 释放内存
}
void UseNewDelete(void)
{
 Obj *a = new Obj; // 申请动态内存并且初始化
 //…
 delete a; // 清除并且释放内存
}
     示例6 用malloc/free和new/delete如何实现对象的动态内存管理

  类Obj的函数Initialize模拟了构造函数的功能,函数Destroy模拟了析构函数的功能。函数UseMallocFree中,由于malloc/free不能执行构造函数与析构函数,必须调用成员函数Initialize和Destroy来完成初始化与清除工作。函数UseNewDelete则简单得多。

  所以我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的"对象"没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。

  既然new/delete的功能完全覆盖了malloc/free,为什么C 不把malloc/free淘汰出局呢?这是因为C 程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。

  如果用free释放"new创建的动态对象",那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放"malloc申请的动态内存",理论上讲程序不会出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。

  7、内存耗尽怎么办?

  如果在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,宣告内存申请失败。通常有三种方式处理"内存耗尽"问题。

  (1)判断指针是否为NULL,如果是则马上用return语句终止本函数。例如:

void Func(void)
{
 A *a = new A;
 if(a == NULL)
 {
  return;
 }
 …
}

  (2)判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行。例如:

void Func(void)
{
 A *a = new A;
 if(a == NULL)
 {
  cout << "Memory Exhausted" << endl;
  exit(1);
 }
 …
}

  (3)为new和malloc设置异常处理函数。例如Visual C 可以用_set_new_hander函数为new设置用户自己定义的异常处理函数,也可以让malloc享用与new相同的异常处理函数。详细内容请参考C 使用手册。

  上述(1)(2)方式使用最普遍。如果一个函数内有多处需要申请动态内存,那么方式(1)就显得力不从心(释放内存很麻烦),应该用方式(2)来处理。

  很多人不忍心用exit(1),问:"不编写出错处理程序,让操作系统自己解决行不行?"

  不行。如果发生"内存耗尽"这样的事情,一般说来应用程序已经无药可救。如果不用exit(1) 把坏程序杀死,它可能会害死操作系统。道理如同:如果不把歹徒击毙,歹徒在老死之前会犯下更多的罪。

  有一个很重要的现象要告诉大家。对于32位以上的应用程序而言,无论怎样使用malloc与new,几乎不可能导致"内存耗尽"。我在Windows 98下用Visual C 编写了测试程序,见示例7。这个程序会无休止地运行下去,根本不会终止。因为32位操作系统支持"虚存",内存用完了,自动用硬盘空间顶替。我只听到硬盘嘎吱嘎吱地响,Window 98已经累得对键盘、鼠标毫无反应。

  我可以得出这么一个结论:对于32位以上的应用程序,"内存耗尽"错误处理程序毫无用处。这下可把Unix和Windows程序员们乐坏了:反正错误处理程序不起作用,我就不写了,省了很多麻烦。

  我不想误导读者,必须强调:不加错误处理将导致程序的质量很差,千万不可因小失大。

void main(void)
{
 float *p = NULL;
 while(TRUE)
 {
  p = new float[1000000];
  cout << "eat memory" << endl;
  if(p==NULL)
   exit(1);
 }
}

  示例7试图耗尽操作系统的内存


8、malloc/free 的使用要点

  函数malloc的原型如下:

void * malloc(size_t size);

  用malloc申请一块长度为length的整数类型的内存,程序如下:

int *p = (int *) malloc(sizeof(int) * length);

  我们应当把注意力集中在两个要素上:"类型转换"和"sizeof"。

  * malloc返回值的类型是void *,所以在调用malloc时要显式地进行类型转换,将void * 转换成所需要的指针类型。

  * malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。我们通常记不住int, float等数据类型的变量的确切字节数。例如int变量在16位系统下是2个字节,在32位下是4个字节;而float变量在16位系统下是4个字节,在32位下也是4个字节。最好用以下程序作一次测试:

cout << sizeof(char) << endl;
cout << sizeof(int) << endl;
cout << sizeof(unsigned int) << endl;
cout << sizeof(long) << endl;
cout << sizeof(unsigned long) << endl;
cout << sizeof(float) << endl;
cout << sizeof(double) << endl;
cout << sizeof(void *) << endl;

  在malloc的"()"中使用sizeof运算符是良好的风格,但要当心有时我们会昏了头,写出 p = malloc(sizeof(p))这样的程序来。

  * 函数free的原型如下:

void free( void * memblock );

  为什么free函数不象malloc函数那样复杂呢?这是因为指针p的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果p是NULL指针,那么free对p无论操作多少次都不会出问题。如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。

   9、new/delete 的使用要点

  运算符new使用起来要比函数malloc简单得多,例如:

int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];

  这是因为new内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new的语句也可以有多种形式。例如

class Obj
{
 public :
  Obj(void); // 无参数的构造函数
  Obj(int x); // 带一个参数的构造函数
  …
}
void Test(void)
{
 Obj *a = new Obj;
 Obj *b = new Obj(1); // 初值为1
 …
 delete a;
 delete b;
}

  如果用new创建对象数组,那么只能使用对象的无参数构造函数。例如

Obj *objects = new Obj[100]; // 创建100个动态对象

  不能写成

Obj *objects = new Obj[100](1);// 创建100个动态对象的同时赋初值1

  在用delete释放对象数组时,留意不要丢了符号'[]'。例如

delete []objects; // 正确的用法
delete objects; // 错误的用法

  后者相当于delete objects[0],漏掉了另外99个对象。

  10、一些心得体会

  我认识不少技术不错的 C /C程序员,很少有人能拍拍胸脯说通晓指针与内存管理(包括我自己)。我最初学习C语言时特别怕指针,导致我开发第一个应用软件(约1万行C代码)时没有使用一个指针,全用数组来顶替指针,实在蠢笨得过分。躲避指针不是办法,后来我改写了这个软件,代码量缩小到原先的一半。

  我的经验教训是:

  (1)越是怕指针,就越要使用指针。不会正确使用指针,肯定算不上是合格的程序员。

  (2)必须养成"使用调试器逐步跟踪程序"的习惯,只有这样才能发现问题的本质。
10 novembre

Standard C

This document provides all the information you need to read and write programs in the Standard C programming language. It describes all aspects of Standard C that are the same on all implementations that conform to the standard for C. Whenever your goal is to produce code that is as portable as possible, this document tells you what you can count on. And by omission, it lets you know what you cannot count on -- nothing in this document is peculiar to any nonstandard dialect of C.

This document is intended as a comprehensive reference for the Standard C programming language, including its support

library . In many ways, this material is best described by what it is not. It is not a history of the development of the language, nor is it a rationale for the current state of the language. Equally, this document is not a tutorial on Standard C, nor is it a lesson on how to write computer programs. It does not describe how to use any particular implementation of Standard C. Consult the documentation that comes with the particular translator (compiler or interpreter) that you are using for specific instructions on translating and executing programs.

Table of Contents

Introduction · Characters · Preprocessing · Syntax · Types · Declarations · Functions · Expressions · Portability

<assert.h> · <ctype.h> · <errno.h> · <float.h> · <iso646.h> · <limits.h> · <locale.h> · <math.h> · <setjmp.h> · <signal.h> · <stdarg.h> · <stddef.h> · <stdio.h> · <stdlib.h> · <string.h> · <time.h> · <wchar.h> · <wctype.h>

C Library Overview · Files and Streams · Formatted Output · Formatted Input

The Standard C language proper is what you write in C

source files. It is best described at several levels of abstraction:

Introduction -- an overview of this document
Characters -- how to interpret character constants and string literals, and how to convert between multibyte characters and wide characters
Preprocessing -- how the translator processes directives and expands macros to produce the C tokens that constitute a translation unit
Syntax -- how the translator parses C tokens into one or more declarations
Types -- how the translator determines the properties of the types you specify within declarations, and how the program represents objects of various types
Declarations -- how the translator interprets the declarations you write to specify types and objects that the program manipulates
Functions -- how the translator interprets the declarations you write to specify the functions that encapsulate all executable code within a C program
Expressions -- how the translator interprets expressions to determine what computations to perform, either at translation time or when the program executes
Portability -- how to write code that is maximally portable across different implementations of Standard C

A C program can call on a large number of functions from the

Standard C library. These functions perform essential services such as input and output. They also provide efficient implementations of frequently used operations. Numerous macro and type definitions accompany these functions to help you to make better use of the library. Most of the information about the Standard C library can be found in the descriptions of the standard headers that declare or define library entities for the program.

The 18 standard headers are:

<assert.h> -- for enforcing assertions when functions execute
<ctype.h> -- for classifying characters
<errno.h> -- for testing error codes reported by library functions
<float.h> -- for testing floating-point type properties
<iso646.h> -- for programming in ISO 646 variant character sets
<limits.h> -- for testing integer type properties
<locale.h> -- for adapting to different cultural conventions
<math.h> -- for computing common mathematical functions
<setjmp.h> -- for executing nonlocal goto statements
<signal.h> -- for controlling various exceptional conditions
<stdarg.h> -- for accessing a varying number of arguments
<stddef.h> -- for defining several useful types and macros
<stdio.h> -- for performing input and output
<stdlib.h> -- for performing a variety of operations
<string.h> -- for manipulating several kinds of strings
<time.h> -- for converting between various time and date formats
<wchar.h> -- for manipulating wide streams and several kinds of strings
<wctype.h> -- for classifying wide characters

Other information on the Standard C library includes:

C Library Overview -- how to use the library, including what happens at program startup and at program termination
Files and Streams -- how to read and write data between the program and files
Formatted Output -- how to generate text under control of a format string
Formatted Input -- how to scan and parse text under control of a format string

See also the

Index.

Copyright © 1989-1996 by P.J. Plauger and Jim Brodie. All rights reserved.http://ccs.ucsd.edu/c/