datasheet
超过460,000+ 应用技术资源下载
pdf

C++高级编程

  • 1星
  • 日期: 2015-05-12
  • 大小: 7.18MB
  • 所需积分:2分
  • 下载次数:0
  • favicon收藏
  • rep举报
  • 分享
  • free评论
标签: C

C++高级编程,容器,重载等

C++高级编程(第 2 版) [比] Marc Gregoire [美] Nicholas A. Solter 著 Scott J. Kleper 侯普秀 郑思遥 译 北京 Marc Gregoire, Nicholas A. Solter, Scott J. Kleper Professional C++, Second Edition EISBN:978-0-470-93244-5 Copyright © 2011 by John Wiley&Sons, Inc. , Indianapolis, Indiana All Rights Reserved. This translation published under license. 本书中文简体字版由 Wiley Publishing, Inc. 授权清华大学出版社出版。未经出版者书面许可,不得以任何方式 复制或抄袭本书内容。 北京市版权局著作权合同登记号 图字:01-2011-6702 本书封面贴有 Wiley 公司防伪标签,无标签者不得销售。 版权所有,侵权必究。侵权举报电话:010-62782989 13701121933 图书在版编目(CIP)数据 C++高级编程(第 2 版)/ (比)格莱戈尔(Gregoire, M.),(美)索尔特(Solter, N.A.),(美)凯乐普(Kleper, S.J.)著; 侯普秀,郑思遥 译. —北京:清华大学出版社,2012.10 书名原文:Professional C++, Second Edition ISBN 978-7-302-29897-7 I. ①C… Ⅱ. ①格… ②索… ③凯… ④侯… ⑤郑… Ⅲ. ①C 语言-程序设计 Ⅳ. ①TP312 中国版本图书馆 CIP 数据核字(2012)第 199353 号 责任编辑:王 军 韩宏志 装帧设计:牛艳敏 责任校对:成凤进 责任印制: 出版发行:清华大学出版社 网 址:http://www.tup.com.cn,http://www.wqbook.com 地 址:北京清华大学学研大厦 A 座 邮 编:100084 社 总 机:010-62770175 邮 购:010-62786544 投稿与读者服务:010-62776969,c-service@tup.tsinghua.edu.cn 质量反馈:010-62772015,zhiliang@tup.tsinghua.edu.cn 印 刷 者: 装 订 者: 经 销: 开 本:185mm×260mm 印 张:59.75 字 数:1603 千字 版 次:2012 年 10 月第 2 版 印 次: 2012 年 10 月第 1 次印刷 印 数:1~4000 定 价:108.00 元 —————————————————————————————————————————————— 产品编号: B 附录 带注解的参考文献 这个附录包含了本书在撰写过程中参阅的与各种不同 C++主题相关的书籍和在线资源,还包含 一些建议作为深入阅读或背景阅读材料的资源。 C++ C++入门 ● Harvey M. Deitel and Paul J. Deitel, C++ How to Program (Seventh Edition), Prentice Hall, 2009, ISBN: 0-136-11726-0. 称为 Deitel 书籍,这本书不要求任何先前的编程经验。 ● Bruce Eckel, Thinking in C++, Volume 1: Introduction to Standard C++ (Second Edition), Prentice Hall, 2000, ISBN: 0-139-79809-9. C++编程的绝佳介绍,要求读者已经了解 C 语言。可以在 www.bruceeckel.com 免费在线阅读。 ● Stanley B. Lippman, Josée Lajoie, and Barbara E. Moo, C++ Primer (Fourth Edition), Addison-Wesley, 2005, ISBN: 0-201-72148-1. 不要求有 C++相关的知识,但是需要有高级面向对象编程相关的经验。 ● Steve Oualline, Practical C++ Programming(Second Edition), O’Reilly, 2003, ISBN: 0-596-00419-2. 入门级的 C++教材,不要求任何编程经验。 ● Walter Savitch, Problem Solving with C++ (Eighth Edition), Addison-Wesley, 2011, ISBN: 0-132-16273-3. 这一本书不要求编程经验。通常作为入门编程课程的教材。 综合 C++ ● Marshall Cline, C++ FAQ LITE, www.parashift.com/c++-faq-lite. C++高级编程(第 2 版) ● Marshall Cline, Greg Lomow, and Mike Girou, C++ FAQs (Second Edition), Addison-Wesley, 1998, ISBN: 0-201-30983-1. 这是来自 comp.lang.c++新闻组的常见问题汇编,可以作为 C++相关具体问题的速查。印刷版包 含的信息比在线版更多,但在线材料中的信息对于大部分专业 C++程序员来说应该够用了。 ● Stephen C. Dewhurst, C++ Gotchas, Addison-Wesley, 2002, ISBN: 0-321-12518-5. 提供了 99 个和 C++编程相关的技巧。 ● Bruce Eckel and Chuck Allison, Thinking in C++, Volume 2: Practical Programming, Prentice Hall, 2003, ISBN: 0-130-35313-2. Eckel 书的第二卷包含了更高级的 C++主题。同样可以在 www.bruceeckel.com 免费在线阅读。 ● Ray Lischner, C++ in a Nutshell, O’Reilly, 2003, ISBN: 0-596-00298-X. 一本 C++参考书,覆盖了从基础知识到高级主题的所有内容。 ● Scott Meyers, Effective C++ (Third Edition): 55 Specific Ways to Improve Your Programs and Designs, Addison-Wesley, 2005, ISBN: 0-321-33487-6. ● Scott Meyers, More Effective C++: 35 New Ways to Improve Your Programs and Designs, Addison-Wesley, 1996, ISBN: 0-201-63371-X. 这两本书对 C++中经常误用和误解的特性提供了极好的技巧和诀窍。 ● Stephen Prata, C++ Primer Plus (Fifth Edition), Sams Publishing, 2004, ISBN: 0-672-32697-4. 最全面的 C++书籍之一。 ● Bjarne Stroustrup, The C++ Programming Language (Special Third Edition), Addison-Wesley, 2000, ISBN: 0-201-70073-5. C++“圣经”,由 C++设计者本人编写。每一位 C++程序员都应该有这本书,但是 C++初学者可 能会感到晦涩难懂。 ● British Standards Institute, The C++ Standard: Incorporating Technical Corrigendum No. 1, Wiley, 2003, ISBN: 0-470-84674-7. 这本书是将近 800 页的密集标准。本书没有解释如何使用 C++,而只有正式的规则。我们不建 议读者本书,除非您真的想理解 C++的每一个细节。 ● http://groups.google.com 上的新闻组,包括 comp.lang.c++.moderated 和 comp.std.c++. 这些新闻组包含了很多有用的信息,但是也有很多不良信息战、辱骂和错误信息。 ● C++ Resources Network,在 www.cplusplus.com 访问。 这个网站包含很多与 C++相关的信息。 I/O 流和字符串 ● Cameron Hughes and Tracey Hughes, Mastering the Standard C++ Classes: An Essential Reference, Wiley, 1999, ISBN: 0-471-32893-6. 帮助理解如何编写自定义 istream 和 ostream 类的好书。 ● Cameron Hughes and Tracey Hughes, Stream Manipulators and Iterators in C++, www.informit.com/articles/article.aspx?p=171014. 这篇由 Mastering the Standard C++ Classes 作者写的好文章解释了如何定义 C++流的自定义运 算符。 918 附录 B 带注解的参考文献 ● Philip Romanik and Amy Muntz, Applied C++: Practical Techniques for Building Better Software, Addison-Wesley, 2003, ISBN: 0-321-10894-9. 除了独特的软件开发建议和 C++相关的知识,这本书还是我们读到过的关于 C++ locale 和 Unicode 支持的最好解释。 ● Joel Spolsky, The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!), www.joelonsoftware.com/articles/Unicode.html. 读完了 Joel 的这篇阐述本地化重要性的文章之后,您会想要再看看 Joel on Software 中的其他 文章。 ● The Unicode Consortium, The Unicode Standard 5.0, Addison-Wesley, 2006, ISBN: 0-321-48091-0. 这是有关 Unicode 的权威书籍,所有使用 Unicode 的开发者都应该有这本书。 ● Unicode, Inc., Where is my Character?, www.unicode.org/standard/where. 查找 Unicode 字符和图表的最佳资源。 ● Wikipedia Universal Character Set, http://en.wikipedia.org/wiki/Universal_Character_Set. 解释 Universal Character Set (UCS)的文章,其中包含 Unicode 标准。 C++标准库 ● Nicolai M. Josuttis, The C++ Standard Library: A Tutorial and Reference, Addison-Wesley, 1999, ISBN: 0-201-37926-0. 本书覆盖了整个标准库,包括 I/O 流和字符串,还包含了容器和算法。这是一本很好的参考书。 ● Scott Meyers, Effective STL: 50 Specific Ways to Improve Your Use of the Standard Template Library, Addison-Wesley, 2001, ISBN: 0-201-74962-9. Meyers 编写这本书的时候采取了和 Effective C++系列书籍一样的思路。这本书以使用 STL 为目 标提供了很多技巧,但不是一本参考书或教程。 ● David R. Musser, Gillmer J. Derge, and Atul Saini, STL Tutorial and Reference Guide (Second Edition), Addison-Wesley, 2001, ISBN: 0-321-70212-3. 本书类似于 Josuttis 的教材,但是只覆盖了标准库中的 STL 部分。 ● Pete Becker, The C++ Standard Library Extensions: A Tutorial and Reference, Addison-Wesley, 2006, ISBN: 0-321-41299-0. 本书解释了 Technical Report1(TR1)添加到 C++标准库的新特性。 ● Stephan T. Lavavej, Standard Template Library (STL), http://channel9.msdn.com/Shows/Going+ Deep/C9-Lectures-Introduction-to-STL-with-Stephan-T-Lavavej. 关于 C++标准模板库的一系列有趣视频。 C++模板 ● Herb Sutter, Sutter’s Mill: Befriending Templates, C/C++ User’s Journal, http://drdobbs.com/cpp/ 184403853. 我们看到的有关编写类的函数模板 friend 的最好的解释文章。 919 C++高级编程(第 2 版) ● David Vandevoorde and Nicolai M. Josuttis, C++ Templates: The Complete Guide, Addison-Wesley, 2002, ISBN: 0-201-73484-2. 有关 C++模板的一切。这本书要求很好的 C++综合背景。 ● David Abrahams and Aleksey Gurtovoy, C++ Template Metaprogramming: Concepts, Tools, and Techniques from Boost and Beyond, Addison-Wesley, 2004, ISBN: 0-321-22725-5. 这本书为程序员日常工作提供了实用的元编程工具和技术。 C++11 ● C++ Standards Committee Papers, www.open-std.org/jtc1/sc22/wg21/docs/papers. 访问由 C++标准委员会编写的大量白皮书。 ● Scott Meyers, Presentation Materials: Overview of the New C++ (C++0x), Artima, 2011, www.artima.com/shop/overview_of_the_new_cpp. 包含了 Scott Meyers 有关新 C++标准的 3 天培训课程的展示材料,这是获得所有 C++11 新特性 列表的很好参考。 ● Wikipedia C++11, http://en.wikipedia.org/wiki/C%2B%2B11. 描述了 C++11 新添加的所有新特性。 ● ECMAScript Language Specifi cation, www.ecma-international.org/publications/files/ECMA-ST/ ECMA-262.pdf. C++11 使用的正则表达式语法和 ECMAScript 语言使用的正则表达式语法一样,这个规范文档 描述了 ECMAScript 语言。 C ● Brian W. Kernighan and Dennis M. Ritchie, The C Programming Language (second edition), Prentice Hall, 1998, ISBN: 0-131-10362-8. 这本书被尊称为 K&R,这是 C 语言的参考书,但是不适合于初次学习。 ● Samuel P. Harbison III and Guy L. Steele Jr., C: A Reference Manual, Fifth Edition, Prentice Hall, 2002, ISBN: 0-130-89592-X. 这本书可以看成是 K&R 一书的替代。这本书没有采用叙述式的将知识嵌在正文中的风格,而 是采用了更规范的参考手册风格。每一位 C 程序员都应该有一本。 ● Peter Prinz, Tony Crawford (Translator), Ulla Kirch-Prinz, C Pocket Reference, O’Reilly, 2002, ISBN: 0-596-00436-2. 关于 C 语言的一切的简明参考。 ● Eric S. Roberts, The Art and Science of C: A Library Based Introduction to Computer Science, Addison-Wesley, 1994, ISBN: 0-201-54322-2. ● Eric S. Roberts, Programming Abstractions in C: A Second Course in Computer Science, Addison-Wesley, 1997, ISBN: 0-201-54541-1. 这两本书以很好的风格介绍了 C 语言编程。常用作编程入门课程的课本。 ● Peter Van Der Linden, Expert C Programming: Deep C Secrets, Prentice Hall, 1994, ISBN: 0-131-77429-8. 920 附录 B 带注解的参考文献 一本具有启发性而且往往会令人兴奋的书,讲解了 C 语言、语言的演进以及内部工作原理。 C++和其他语言的集成 ● Ian F. Darwin, Java Cookbook (Second Edition), O’Reilly, 2004, ISBN: 0-596-00701-9. 这本书提供了使用 JNI 将 Java 和其他语言集成的分步指南,包括 C++。 算法和数据结构 ● Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein, Introduction to Algorithms (Third Edition), The MIT Press, 2009, ISBN: 0-262-03384-4. 这本书是最流行的算法书籍之一,涵盖了所有常见的数据结构和算法。 ● Donald E. Knuth, The Art of Computer Programming Volume 1: Fundamental Algorithms (Third Edition), Addison-Wesley, 1997, ISBN: 0-201-89683-4. ● Donald E. Knuth, The Art of Computer Programming Volume 2: Seminumerical Algorithms (Third Edition), Addison-Wesley, 1997, ISBN: 0-201-89684-2. ● Donald E. Knuth, The Art of Computer Programming Volume 3: Sorting and Searching (Second Edition), Addison-Wesley, 1998, ISBN: 0-201-89685-0. ● Donald E. Knuth, The Art of Computer Programming Volume 4A: Combinatorial Algorithms Part 1, Addison-Wesley, 2011, ISBN: 0-201-03804-8. 如果您喜欢数学的严谨,那么 Knuth 的这四卷巨著是最好的算法和数据结构教材了。但是,如 果没有本科程度的数学知识和理论计算机科学相关的知识,可能不好领悟这些书。 ● Kyle Loudon, Mastering Algorithms with C, O’Reilly, 1999, ISBN: 1-565-92453-3. 一本通俗易懂的数据结构和算法参考书。 随机数 ● Eric Bach and Jeffrey Shallit, Algorithmic Number Theory, Vol. 1: Efficient Algorithms, The MIT Press, 1996, ISBN: 0-262-02405-5. ● Oded Goldreich, Modern Cryptography, Probalistic Proofs and Pseudorandomness, Springer, 2010, ISBN: 3-642-08432-X. 这两本书都讲解了计算伪随机性的理论。 ● Wikipedia Mersenne Twister, http://en.wikipedia.org/wiki/Mersenne_twister. 通过梅森旋转算法生成伪随机数的数学理论。 921 C++高级编程(第 2 版) 开源软件 ● The Open Source Initiative:www.opensource.org. ● The GNU Operating System — Free Software Foundation:www.gnu.org. 这是两个主要的开源运动的网页,解释了它们的哲学,并提供了有关获得开源软件以及为开源 软件开发做贡献的信息。 ● SourceForge:www.sourceforge.net. 这个网站为很多开源项目提供了服务。这是查找有用开源软件的极好资源。 ● www.codeguru.com 和 www.codeproject.com. 查找免费库和可以在自己的项目中重用的代码的极佳资源。 软件工程方法学 ● Barry W. Boehm, TRW Defense Systems Group, A Spiral Model of Software Development and Enhancement, IEEE Computer, 21(5): 61–72, 1988. 这篇重要的论文描述了当时软件开发的状态,并提出了 Spiral 模型。 ● Kent Beck and Cynthia Andres, Extreme Programming Explained: Embrace Change (Second Edition), Addison-Wesley, 2004, ISBN: 0-321-27865-8. 推崇极限编程为新型软件开发方法的一系列书籍中的一本。 ● Robert T. Futrell, Donald F. Shafer, and Linda Isabell Shafer, Quality Software Project Management, Prentice Hall, 2002, ISBN: 0-130-91297-2. 负责管理软件开发过程的人员的指南。 ● Robert L. Glass, Facts and Fallacies of Software Engineering, Addison-Wesley, 2002, ISBN: 0-321-11742-5. 这本书讨论了软件开发中几个不同的方面,并且展示了一些隐藏的道理。 ● Philippe Kruchten, The Rational Unified Process: An Introduction (Third Edition), Addison-Wesley, 2003, ISBN: 0-321-19770-4. 对 RUP 进行了概述,包括其使命和过程。 ● Edward Yourdon, Death March (Second Edition), Prentice Hall, 2003, ISBN: 0-131-43635-X. 软件开发中的政策和现实,是一本极佳的启蒙读物。 ● Rational Unified Process from IBM, www3.software.ibm.com/ibmdl/pub/software/rational/web/ demos/viewlets/rup/runtime/index.html. 这个 IBM 的网站包含了大量关于 RUP 的信息,包括这个 URL 中的交互式陈述。 ● Mike Cohn, Succeeding with Agile: Software Development Using Scrum, Addison-Wesley, 2009, ISBN: 0-321-57936-4. 开始使用 Scrum 方法的优秀指南。 ● Wikipedia Scrum, http://en.wikipedia.org/wiki/Scrum_(development). Scrum 方法的详细讨论。 922 附录 B 带注解的参考文献 ● Manifesto for Agile Software Development, http://agilemanifesto.org/. 完整的敏捷开发宣言。 ● Wikipedia Revision control, http://en.wikipedia.org/wiki/Revision_control. 解释了版本控制系统背后的概念,包括一些可用的解决方案。 编程风格 ● Martin Fowler, Kent Beck, John Brant, William Opdyke, and Don Roberts, Refactoring: Improving the Design of Existing Code, Addison-Wesley, 1999, ISBN: 0-201-48567-2. ● 讲解识别和改进糟糕代码实践的经典书籍。 ● James Foxall, Practical Standards for Microsoft Visual Basic .NET, Microsoft Press, 2002, ISBN: 0-735-61356-7. 讲解 Microsoft Windows 编程风格的原则,使用 Visual Basic .NET。 ● Diomidis Spinellis, Code Reading: The Open Source Perspective, Addison-Wesley, 2003, ISBN: 0-201-79940-5. ● 这本独特的书从相反的角度讲解了编程风格的问题,教会读者正确地阅读代码以成为一名 更优秀的程序员。 ● Dimitri van Heesch, Doxygen, www.stack.nl/~dimitri/doxygen/index.html. 一个高度可定制的程序,从源代码和注释生成文档。 ● John Aycock, Reading and Modifying Code, John Aycock, 2008, ISBN 0-980-95550-5. 内容简练,妙趣横生,给出了代码最常见操作相关的建议:包括阅读、修改、测试、调试和编 写代码。 ● Wikipedia Code Refactoring, http://en.wikipedia.org/wiki/Refactoring. 讨论代码重构的意义,包括一些重构的技术。 计算机体系结构 ● David A. Patterson and John L. Hennessy, Computer Organization and Design: The Hardware/Software Interface (Fourth Edition), Morgan Kaufmann, 2008, ISBN: 0-123-74493-8. ● John L. Hennessy and David A. Patterson, Computer Architecture: A Quantitative Approach (Fourth Edition), Morgan Kaufmann, 2006, ISBN: 0-123-70490-1. 这两本书提供了大部分软件工程师需要知道的所有关于计算机体系结构的知识。 效率 ● Dov Bulka and David Mayhew, Efficient C++: Performance Programming Techniques, Addison-Wesley, 1999, ISBN: 0-201-37950-3. 923 C++高级编程(第 2 版) 少有的基本专门讨论高效 C++编程的书之一,既包含了语言层次的效率,又包含了设计层次的 效率。 ● GNU gprof, www.gnu.org/software/binutils/. 关于 gprof 性能剖析工具的信息。 测试 ● Elfriede Dustin, Effective Software Testing: 50 Specific Ways to Improve Your Testing, Addison-Wesley, 2002, ISBN: 0-201-79429-2. 尽管这本书面向 QA 专业人士,但是任何软件工程师都可以通过其中介绍的软件测试过程获益。 调试 ● The GNU DeBugger (GDB):www.gnu.org/software/gdb/gdb.html. GDB 是一个极佳的符号调试器。 ● Valgrind:http://valgrind.org/. Linux 下的一个开源内存调试工具。 ● Microsoft Application Verifier:http://msdn.microsoft.com/en-us/library/aa480483.aspx. 一个用于 C++代码的运行时验证工具,能帮助找到微妙的编程错误和安全性问题,用一般的应 用程序测试技术很难找到这些问题。 设计模式 ● Andrei Alexandrescu, Modern C++ Design: Generic Programming and Design Patterns Applied, Addison-Wesley, 2001, ISBN: 0-201-70431-5. 为 C++编程的高度可重用的代码和模式提供了方法。 ● Cunningham and Cunningham, The Portland Pattern Repository, www.c2.com/cgi/wiki? WelcomeVisitors. 可以花整天的时间泡在这个关于设计模式的可编辑社区网站上。 ● Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1994, ISBN: 0-201-63361-2. 称为“四人团队”书籍因为有 4 位作者),这是设计模式的开山之作。 ● Wikipedia Design Patterns, http://en.wikipedia.org/wiki/Design_pattern_(computer_science). 描述了大量计算机编程中使用的设计模式。 924 附录 B 带注解的参考文献 操作系统 ● Abraham Silberschatz, Peter B. Galvin, and Greg Gagne, Operating System Concepts (Eighth Edition), Wiley, 2008, ISBN: 0-470-12872-0. 讨论操作系统的优秀著作,包括多线程的问题,例如死锁和竞争条件。 多线程编程 ● Anthony Williams, C++ Concurrency in Action: Practical Multithreading, Manning Publications, 2011, ISBN: 1-933-98877-0. 有关多线程编程实践的一本优秀书籍,包含最新的 C++11 线程库。 ● Cameron Hughes and Tracey Hughes, Professional Multicore Programming: Design and Implementation for C++ Developers, Wrox, 2008, ISBN: 0-470-28962-7. 这本书适用于想要转移到多核编程的不同层次的开发人员。 ● Maurice Herlihy and Nir Shavit, The Art of Multiprocessor Programming, Morgan Kaufmann, 2008, ISBN: 0-123-70591-6. 一本关于多处理器和多核系统编程的优秀著作。 ● Wikipedia POSIX Threads, http://en.wikipedia.org/wiki/POSIX_Threads. ● Boost Threads, www.boost.org. 讲解了如何在编译器还不支持 C++11 线程库的情况下使用 POSIX 线程和 Boost 线程。 925 技术编辑简介 Joseph M. Newcomer 在卡内基梅隆大学(CMU)获得计算机科学博士学位。之后在 CMU 任教, 担任 CMU 软件工程学院的一位资深科学家,同时他还是一位独立软件开发者。他连续 16 年获得 C/C++/MFC 领域的 Microsoft MVP。他是两本 Microsoft Windows 编程书籍的合作者:Win32 Programming(与 Brent Rector 合著,Addison-Wesley,1997)和 Developing Windows NT Device Drivers(与 Ed Dekker 合著,Addison-Wesley,1999)。Joseph 是上世纪 70 年代初桌面出版和数据库出版领域的 技术先驱之一。 前言 多年来,C++都是编写性能卓越、功能强大的企业级面向对象程序的事实标准语言。尽管 C++ 语言已经风靡全球,但是这个语言却非常难完全掌握。专业 C++程序员使用了一些简单但高效的技 术,这些技术并未出现在传统教材中;即使是经验丰富的 C++程序员也不了解 C++中的某些很有用 的特性。 编程书籍往往重点描述语言的语法,而不是语言在真实世界中的应用。典型的 C++教材在每一 章中介绍了语言中的大部分知识,讲解语法并列举示例。本书不遵循这个模式。本书并不是讲解语 言的大量细节并给出少量真实世界的场景,而是教您如何在真实世界中使用 C++。本书还会披露一 些鲜为人知的特性,使用这些特性可以让编程更简单;还讲解了可重用的编码模式,模式是区分编 程新手和专业程序员的标志。 本书读者对象 就算您使用了多年的 C++,您仍然有可能不熟悉 C++的一些高级特性,或者仍然不具有使用这 个语言的完整能力。也许您编写过实用的 C++代码,但还想学习更多有关使用 C++设计和良好的编 程风格的内容。也许您是 C++新手,想在入门的时候就掌握“正确”的编程方式。本书能满足上述 需求,能将您的 C++技能提升到专业水准。 由于本书专注于从对 C++具有基本或中等了解水平蜕变为一名专业的 C++程序员的过程,所以 本书假设您对这个语言具有一定程度的认识。第 1 章涵盖了 C++的一些基础知识,可以当作复习材 料,但是不能替代实际的语言培训和语言使用手册。如果您刚刚开始接触 C++,但有很丰富的 C 语 言经验,那么您应该能从第 1 章获得大部分所需的知识。 不管是那种情况,您都应该有很好的编程基础。您应该知道循环、函数和变量。您应该知道如 何组织一个程序,而且应该熟悉基本技术,例如递归。您应该了解一些常见数据结构,例如哈希表 和队列,以及有用的算法,例如排序和搜索。您不需要预先了解有关面向对象编程的知识—— 这是 第 3 章讲解的内容。 您还应该熟悉开发代码时使用的编译器。这本书没有提供使用具体编译器的指南。请参阅编译 器自带的指南。 本书主要内容 阅读本书是学习 C++语言的一种方法,通过阅读本书既能提升编码质量,又能提升编程效率。 本书贯穿了对 C++11 新特性的讨论。这些新的 C++11 特性并没有分离在几个章节中,而是贯穿于 C++高级编程(第 2 版) 全书,在有必要的情况下,几乎所有的例子都已经更新为使用这些新特性。 提示:C++11 的完整 ISO 名称为 ISO/IEC 14882:2011(E),是最新的 C++标准。 C++11 标准化的过程从本世纪初就开始了。在这个过程中,标准的草案曾被称为 C++0x。 本书不仅讲解 C++语法和语言特性,还强调了编程方法学、可重用的设计模式以及良好的编程 风格。本书讲解的方法学覆盖了整个软件开发过程—— 从设计和编码,到测试、调试以及团队合作。 这种方法可以让您掌握 C++语言及其语言的独特特性,还能够在大型软件开发中充分利用 C++语言 的强大功能。 想象一下如果有人学习了 C++所有的语法但是没有看过一个 C++例子的情形。他所了解的知识 会让他处于非常危险的境地。如果没有示例的引导,他可能会认为所有源代码都要放在程序的 main() 函数中,还有可能认为所有变量都应该为全局变量—— 这些都不是良好的编程实践。 专业的 C++程序员除了理解语法之外,还要正确理解语言的使用方式。他们知道良好设计的重 要性、面向对象编程的理论以及使用现有库的最佳方式。他们还开发了大量有用的代码并了解可重 用的思想。 通过阅读和理解本书的内容,您也能成为一名专业的 C++程序员。您在 C++方面的知识会得到 扩充,将会接触到鲜为人知的和常被误解的语言特性。您还将领略面向对象的设计,掌握卓越的调 试技能。最重要的或许是,通过本书的阅读,您会了解到大量“可重用”思想,并将这种思想贯彻 到日常工作中。 有很多好的理由让您努力成为一名专业的 C++程序员,而非只是泛泛了解 C++的程序员。了解 语言的真正工作原理可以提升代码的质量。了解不同的编程方法学和过程可以让您更好地和团队合 作。探索可重用的库和常用的设计模式可以提升您日常工作的效率,并帮助您避免重新造轮子。所 有这些学习课程都在帮助您成为更优秀的程序员,同时成为更有价值的雇员。尽管这本书不能保证 您升职,但是肯定不会有坏处。 本书结构 本书的正文部分包括四个部分,另有三个附录。 第 I 部分是一个 C++基础速成教程,确保读者掌握 C++的基础知识。在速成教程之后,第 I 部 分介绍了 C++设计方法学。您会了解到设计的重要性、面向对象方法学、代码重用的重要性以及如 何编写清晰易读的 C++代码。 第 II 部分从专业的角度概述 C++技术。您将学习如何创建可重用的类,以及如何利用重要的语 言特性,例如继承。您还会学习这个语言的一些不同寻常之处、输入和输出技术、专业级别的错误 处理以及字符串和正则表达式的使用。这一部分还讲解了 C++标准库,包括容器、迭代器、算法以 及如何定制和扩充标准库以满足自己的需求。您还会学习标准中其他一些库,例如处理时间的库和 处理随机数的库。 第 III 部分讲解如何最大限度地使用 C++。本书这一部分揭示了 C++中神秘的部分,并且描述 VI 前言 了如何使用这些更高级的特性。您会学习在 C++中如何恰到好处地管理内存、如何实现高级的运算 符重载、如何编写模板以及如何通过多线程编程来充分利用多处理器和多核系统。 第 IV 部分重点介绍如何编写企业级质量的软件。您会学习当今编程组织使用的工程实践;软 件测试概念,例如单元测试和回归测试;C++程序的调试技术;如何编写高效的 C++代码;跨语言 和跨平台编程的解决方案;以及如何在代码中综合使用设计技术、框架和概念性的面向对象设计 模式。 本书最后是三个附录。附录 A 列出在 C++技术面试中取得成功的指南(按章分解内容),附录 B 是带注解的参考文献列表,附录 C 则总结了标准中的 C++头文件。在本书网站 www.wrox.com 上的 补充材料中可以找到一份实用的 C++标准库参考指南。这个参考指南在本书中称为“网站上的标准 库参考资源”。 使用本书的条件 要使用这本书,您只需要一台带有 C++编译器的计算机。这本书只关注 C++中的标准部分,而 没有任何编译器厂商相关的扩展。 本书包含了 C++11 标准引入的新特性。在撰写本书的时候,还没有编译器能支持 C++11 所有 的新特性。本书通过运行在 Linux 上的 GCC 4.6 和运行在 Windows 上的 Microsoft Visual C++ 2010 测试所有示例代码。GCC 4.6 支持很多新特性,但并非能支持所有的新特性。使用了以下 C++11 特 性的例子不能通过当前的 GCC 4.6 进行测试: ● final 和 override 关键字 ● 用户定义的文本 ● 类内非静态数据成员的初始化 ● 模板别名 ● 构造函数委托 ● 构造函数继承 本书用于测试的第二个编译器是 Microsoft Visual C++ 2010,然而,这个编译器支持的 C++11 特性比 GCC 4.6 还少。VC++ 2010 中缺少的特性主要包括基于范围的 for 循环、统一初始化、可变 参数模板以及线程库等。 本书网站 www.wrox.com 上的附加材料中包含一个额外的附录,这个附录列出了 所有的 C++11 特性,以及某个编译器是否支持某项特性的信息。 约定 为了帮助您更好地理解正文内容,全书中使用了一些约定。 像这样的框中包含了和周围正文信息相关的重要的、应该牢记的信息。 VII C++高级编程(第 2 版) 与当前讨论的内容相关的技巧、提示、小窍门和旁白放在这样的框中。 源代码 在读者学习本书中的示例时,可以手动输入所有的代码,也可以使用本书附带的源代码文件。 本书使用的所有源代码都可以从本书合作站点 http://www.wrox.com/或 http://www.tupwk.com.cn/ downpage 上下载。登录到站点 http://www.wrox.com/,使用 Search 工具或使用书名列表就可以找到 本书。接着单击本书细目页面上的 Download Code 链接,就可以获得所有源代码。 注释:由于许多图书的标题都很类似,因此按 ISBN 搜索是最简单的,本书英文版的 ISBN 是 978-0-470-93244-5。 在下载了代码后,只需用自己喜欢的解压缩软件对它进行解压缩即可。另外,也可以进入 http://www.wrox.com/dynamic/books/download.aspx 上的 Wrox 代码下载主页,查看本书和其他 Wrox 图书的所有代码。 勘误表 尽管我们已经尽了各种努力来保证文章或代码中不出现错误,但是错误总是难免的,如果您在 本书中找到了错误,例如拼写错误或代码错误,请告诉我们,我们将非常感激。通过勘误表,可以 让其他读者避免受挫,当然,这还有助于提供更高质量的信息。 请给 wkservice@vip.163.com 发电子邮件,我们就会检查您的信息,如果是正确的,我们将在本 书的后续版本中采用。 要在网站上找到本书的勘误表,可以登录 http://www.wrox.com,通过 Search 工具或书名列表查 找本书,然后在本书的细目页面上,单击 Book Errata 链接。在这个页面上可以查看到 Wrox 编辑已提交 和粘贴的所有勘误项。完整的图书列表还包括每本书的勘误表,网址是 www.wrox.com/misc-pages/ booklist.shtml。 P2P.WROX.COM 要与作者和同行讨论,请加入 p2p.wrox.com 上的 P2P 论坛。这个论坛是一个基于 Web 的系统, 便于您张贴与 Wrox 图书相关的消息和相关技术,与其他读者和技术用户交流心得。该论坛提供了 订阅功能,当论坛上有新的消息时,它可以给您传送感兴趣的论题。Wrox 作者、编辑和其他业界专 家和读者都会到这个论坛上来探讨问题。 VIII 前言 在 http://p2p.wrox.com 上,有许多不同的论坛,它们不仅有助于阅读本书,还有助于开发自己 的应用程序。要加入论坛,可以遵循下面的步骤: (1) 进入 p2p.wrox.com,单击 Register 链接。 (2) 阅读使用协议,并单击 Agree 按钮。 (3) 填写加入该论坛所需要的信息和自己希望提供的其他信息,单击 Submit 按钮。 (4) 您会收到一封电子邮件,其中的信息描述了如何验证账户,完成加入过程。 注释:不加入 P2P 也可以阅读论坛上的消息,但要张贴自己的消息,就必须加入 该论坛。 加入论坛后,就可以张贴新消息,响应其他用户张贴的消息。可以随时在 Web 上阅读消息。如 果要让该网站给自己发送特定论坛中的消息,可以单击论坛列表中该论坛名旁边的 Subscribe to this Forum 图标。 关于使用 Wrox P2P 的更多信息,可阅读 P2P FAQ,了解论坛软件的工作情况以及 P2P 和 Wrox 图书的许多常见问题。要阅读 FAQ,可以在任意 P2P 页面上单击 FAQ 链接。 IX 致谢 在此要感谢 John Wiley & Sons&Wrox Press 的编辑和产品团队,感谢他们的支持。特别要感谢 Wiley 的执行编辑 Robert Elliott 让我有机会撰写本书;感谢 Wiley 的高级项目编辑 Adaobi Obi Tulton 对这个项目的管理。 还要感谢本书第一版的作者 Nicholas A. Solter 和 Scott J. Kleper,你们让我这本第二版有了很好 的工作基础。 特别感谢技术编辑 Joseph M. Newcomer,感谢他一丝不苟地对本书进行审阅。他提出很多具有 建设性的评论和意见,将本书的质量推进到更高的水平。 衷心感谢 Ordina Belgium(www.ordina.be),这是我在编写这本书的时候所服务的公司,感谢这家 公司给予的慷慨支持。 当然,还要感谢女友 Zulija N.以及我的父母和兄长对我的支持和关爱,你们的支持对这本书的 完成至关重要。 最后,我要感谢各位亲爱的读者的信任;本书也必将使各位读者取得圆满的学习效果。 作者简介 Marc Gregoire 是一名软件工程师。他毕业于比利时鲁文的天主教大学业, 获得计算机科学工程硕士学位。之后,他在该大学获得人工智能的优等硕士学位。 完成学业后,他开始为大型软件咨询公司 Ordina Belgium 工作。他曾在 Siemens 和 Nokia Siemens Networks 为大型电信运营商提供有关在 Solaris 上运行关键 2G 和 3G 软件的咨询服务。这份工作要求与来自南美、美国、欧洲、中东、非洲和亚洲 的国际团队合作。Marc 目前在 Nikon Metrology 任职,负责开发 3D 扫描软件。 他的主要技术专长是 C/C++,特别是 Microsoft VC++和 MFC 框架。除了 C/C++之外,Marc 还喜欢 C#,并且会用 PHP 创建网页。除了在 Windows 上开发的主要兴趣之外,他还擅长在 Linux 平台上 开发 24×7 运行的 C++程序;例如 EIB 家庭自动化监控软件。2007 年 4 月,因为在 Visual C++方面 的专业才能他获得了年度 Microsoft MVP 称号。Marc 还是 CodeGuru 论坛的活跃分子(id 为 Marc G), 并且为 CodeGuru 撰写了一些文章和 FAQ 条目。他还编写了一些自由软件和共享软件,并通过他的 网站 www.nuonsoft.com 发布。他还在 www.nuonsoft.com/blog/维护了一个博客。 Nicholas A. Solter 是一名计算机程序员,开发的软件范围很广,包括系统软 件、游戏、网络服务和其他很多类型。他在 Sun Microsystem 的高可用集群上所做 的工作获得了 3 项专利,还就此在国际并行和分布式处理会议上发表了一篇技 术论文。在 Sun 的时候,他还喜欢参与 OpenSolaris,他还是 OpenSolaris Bible (Wiley, 2009)的第一作者。现在重新从事 Web 开发,他很开心地再次和 Scott 一起 在 Context Optional 工作。 Nick 在斯坦福大学学习计算机科学,他在这所大学获得了本科和理科硕士学位,他的主要研究 领域是计算机系统。他曾在富勒顿社区大学讲授了一年的 C++课程。 Nick 和他的妻子和两个孩子生活在美丽的科罗拉多,他在科罗拉多享受着雪上运动的乐趣。 Scott J. Klerper 在小学就开始了他的编程生涯,那时他在 Tandy TRS-80 上 用 BASIC 语言编写探险游戏。作为他所在高中的 Mac 迷,Scott 转向了更高级 的语言,并且发布了一些屡获殊荣的共享软件。 Scott 加入了斯坦福大学,并且在这所大学获得了本科和计算机科学的理学 硕士学位,主要研究领域是人机交互。在上大学的时候,Scott 是一门涉及编程 入门、面向对象设计、数据结构、GUI 框架和小组项目的课程的助教。他之后 在斯坦福的一门课程采用这本书作为课本。 毕业后,Scott 是几家公司创始团队中的首席工程师。2006 年,Scott 与他人合伙创建了 Context Optional, Inc.,这是一家提供社会营销技术的市场领先的供应商。 在工作之余,Scott 还热衷于在线购物、阅读和弹吉他。 目录 第Ⅰ部分 专业的 C++简介 第1章 1.1 1.2 1.3 1.4 1.5 1.6 C++速成....................................... 3 C++基础知识................................... 3 1.1.1 小程序的“hello world”..............3 1.1.2 名称空间 .......................................6 1.1.3 变量 ...............................................8 1.1.4 运算符 ...........................................9 1.1.5 类型 ............................................ 12 1.1.6 条件 ............................................ 14 1.1.7 循环 ............................................ 16 1.1.8 数组 ............................................ 18 1.1.9 函数 ............................................ 19 深入研究 C++................................21 1.2.1 指针以及动态内存..................... 21 1.2.2 C++中的字符串 ......................... 24 1.2.3 引用 ............................................ 25 1.2.4 异常 ............................................ 26 1.2.5 const 的多种用法 ....................... 27 作为面向对象语言的 C++............28 标准库 ............................................ 30 第一个有用的 C++程序................31 1.5.1 雇员记录系统............................. 31 1.5.2 Employee 类 ............................... 32 1.5.3 Database 类................................. 35 1.5.4 用户界面 .................................... 38 1.5.5 评估程序 .................................... 40 本章小结 ........................................ 41 第 2 章 设计专业的 C++程序..................43 2.1 程序设计概述 ................................ 43 2.2 程序设计的重要性 ........................ 44 2.3 C++设计的特点.............................46 2.4 C++设计的两个原则.....................47 2.4.1 抽象 .............................................47 2.4.2 重用 .............................................48 2.5 重用代码 ........................................49 2.5.1 关于术语的说明 .........................50 2.5.2 决定是否重用代码 .....................50 2.5.3 重用代码的策略 .........................52 2.5.4 绑定第三方应用程序..................56 2.5.5 开放源代码库 .............................56 2.5.6 C++标准库..................................57 2.6 设计模式以及技巧 ........................58 2.7 设计一个国际象棋程序 ................58 2.7.1 需求 .............................................58 2.7.2 设计步骤 .....................................59 2.8 本章小结 ........................................63 第3章 3.1 3.2 3.3 3.4 面向对象设计............................. 65 过程化的思考方式 ........................65 面向对象思想 ................................66 3.2.1 类 .................................................66 3.2.2 组件 .............................................66 3.2.3 属性 .............................................67 3.2.4 行为 .............................................67 3.2.5 综合考虑 .....................................67 生活在对象世界里 ........................68 3.3.1 过度使用对象 .............................69 3.3.2 过于通用的对象 .........................69 对象之间的关系 ............................70 3.4.1 “有一个”关系 .........................70 3.4.2 “是一个”关系(继承)...............71 C++高级编程(第 2 版) 3.4.3 “有一个”与“是一个” 的区别 ........................................ 73 3.4.4 Not-a 关系 .................................. 75 3.4.5 层次结构 .................................... 76 3.4.6 多重继承 .................................... 77 3.4.7 混入类 ........................................ 78 3.5 抽象................................................78 3.5.1 接口与实现 ................................ 78 3.5.2 决定公开的接口......................... 78 3.5.3 设计成功的抽象......................... 80 3.6 本章小结 ........................................ 81 第4章 4.1 4.2 4.3 设计可重用代码 ......................... 83 重用哲学 ........................................ 83 如何设计可重用的代码 ................ 84 4.2.1 使用抽象 .................................... 84 4.2.2 构建理想的重用代码................. 85 4.2.3 设计有用的接口......................... 89 4.2.4 协调通用性以及使用性............. 92 本章小结 ........................................ 93 第 5 章 编码风格 ....................................95 5.1 良好外观的重要性 ........................ 95 5.1.1 事先考虑 .................................... 95 5.1.2 良好风格的元素......................... 96 5.2 为代码编写文档 ............................ 96 5.2.1 使用注释的原因......................... 96 5.2.2 注释的风格 ................................ 99 5.2.3 本书的注释 .............................. 103 5.3 分解..............................................103 5.3.1 通过重构分解........................... 104 5.3.2 通过设计分解........................... 104 5.3.3 本书中的分解........................... 104 5.4 命名..............................................104 5.4.1 选择一个恰当的名称............... 105 5.4.2 命名约定 .................................. 105 5.5 使用具有风格的语言特性 .......... 107 5.5.1 使用常量 .................................. 108 5.5.2 使用引用代替指针................... 108 5.5.3 使用自定义异常 ...................... 108 5.6 格式 ..............................................109 5.6.1 关于大括号对齐的争论........... 109 5.6.2 关于空格以及圆括号的争论... 110 5.6.3 空格以及制表符 ...................... 110 5.7 风格的挑战 .................................. 110 5.8 本章小结 ...................................... 111 第Ⅱ部分 专业的 C++编码方法 第6章 6.1 6.2 6.3 6.4 熟悉类和对象........................... 115 电子表格示例介绍 ...................... 115 编写类 .......................................... 116 6.2.1 类定义 ...................................... 116 6.2.2 定义方法 .................................. 118 6.2.3 使用对象 .................................. 122 对象的生命周期 ..........................123 6.3.1 创建对象 .................................. 124 6.3.2 销毁对象 .................................. 139 6.3.3 对象赋值 .................................. 140 6.3.4 复制以及赋值的区别............... 142 本章小结 ......................................144 第 7 章 掌握类与对象........................... 145 7.1 对象的动态内存分配 ..................145 7.1.1 Spreadsheet 类 .......................... 146 7.1.2 使用析构函数释放内存........... 147 7.1.3 处理复制以及赋值 .................. 148 7.2 定义数据成员的类型 ..................155 7.2.1 静态数据成员 .......................... 155 7.2.2 常量数据成员 .......................... 157 7.2.3 引用数据成员 .......................... 158 7.2.4 常量引用数据成员 .................. 159 7.3 与方法有关的更多内容 ..............159 7.3.1 静态方法 .................................. 159 7.3.2 const 方法................................. 160 7.3.3 方法重载 .................................. 162 7.3.4 默认参数 .................................. 163 7.3.5 内联方法 .................................. 164 7.4 嵌套类 ..........................................165 XII 目录 7.5 类内的枚举类型 .......................... 167 7.6 友元..............................................168 7.7 运算符重载 .................................. 169 7.7.1 示例:为 SpreadsheetCell 实现加法 .................................. 169 7.7.2 重载算术运算符....................... 174 7.7.3 重载比较运算符....................... 176 7.7.4 创建具有运算符重载的类型... 177 7.8 创建稳定的接口 .......................... 178 7.9 本章小结 ...................................... 181 第 8 章 揭秘继承技术...........................183 8.1 使用继承构建类 .......................... 183 8.1.1 扩展类 ...................................... 184 8.1.2 重写方法 .................................. 187 8.2 使用继承重用代码 ...................... 190 8.2.1 WeatherPrediction 类 ................ 190 8.2.2 在子类中添加功能................... 191 8.2.3 在子类中替换功能................... 192 8.3 利用父类 ...................................... 193 8.3.1 父类构造函数........................... 193 8.3.2 父类的析构函数....................... 195 8.3.3 使用父类方法........................... 196 8.3.4 向上转型以及向下转型........... 198 8.4 继承与多态性 .............................. 200 8.4.1 回到电子表格........................... 200 8.4.2 设计多态性的电子表格 单元格 ...................................... 200 8.4.3 电子表格单元格的基类........... 201 8.4.4 独立的子类 .............................. 203 8.4.5 利用多态性 .............................. 205 8.4.6 考虑将来 .................................. 206 8.5 多重继承 ...................................... 207 8.5.1 从多个类继承........................... 207 8.5.2 名称冲突以及歧义基类........... 208 8.6 有趣而晦涩的继承问题 .............. 211 8.6.1 修改重写方法的特征............... 211 8.6.2 继承构造函数(仅限 C++11).... 215 8.6.3 重写方法时的特殊情况........... 218 8.6.4 子类中的复制构造函数以及 赋值运算符 .............................. 224 8.6.5 virtual 的真相........................... 225 8.6.6 运行时类型工具 ...................... 228 8.6.7 非 public 继承........................... 229 8.6.8 虚基类 ...................................... 230 8.7 本章小结 ......................................231 第 9 章 理解灵活而奇特的 C++............ 233 9.1 引用 ..............................................233 9.1.1 引用变量 .................................. 234 9.1.2 引用数据成员 .......................... 236 9.1.3 引用参数 .................................. 236 9.1.4 引用作为返回值 ...................... 238 9.1.5 使用引用还是指针 .................. 238 9.1.6 右值引用(仅限 C++11)............ 241 9.2 关键字的疑问 ..............................246 9.2.1 const 关键字............................. 246 9.2.2 static 关键字............................. 250 9.2.3 非局部变量的初始化顺序....... 254 9.3 类型以及类型转换 ......................254 9.3.1 typedef ...................................... 254 9.3.2 函数指针 typedef...................... 255 9.3.3 类型别名(仅限 C++11)............ 256 9.3.4 类型转换 .................................. 257 9.4 作用域解析 ..................................261 9.5 C++11 ...........................................262 9.5.1 统一初始化 .............................. 262 9.5.2 可选函数语法 .......................... 264 9.5.3 空指针文本 .............................. 265 9.5.4 尖括号 ...................................... 265 9.5.5 初始化列表 .............................. 266 9.5.6 显式转换运算符 ...................... 266 9.5.7 特性 .......................................... 267 9.5.8 用户定义的字面量 .................. 268 9.6 头文件 ..........................................270 9.7 C 的实用工具...............................271 XIII C++高级编程(第 2 版) 9.7.1 变长参数列表........................... 271 9.7.2 预处理器宏 .............................. 273 9.8 本章小结 ...................................... 274 第 10 章 10.1 10.2 10.3 10.4 10.5 10.6 10.7 错误处理 ................................275 错误与异常 ................................ 275 10.1.1 异常的含义 ........................... 276 10.1.2 C++中异常的优点 ................ 276 10.1.3 C++中异常的缺点 ................ 277 10.1.4 我们的建议 ........................... 277 异常机制 .................................... 277 10.2.1 抛出并捕获异常 ................... 278 10.2.2 异常类型 ............................... 281 10.2.3 抛出并捕获多个异常 ........... 283 10.2.4 未捕获的异常 ....................... 285 10.2.5 抛出列表 ............................... 287 异常与多态性 ............................ 291 10.3.1 标准异常体系 ....................... 291 10.3.2 在类层次结构中捕获异常 ... 293 10.3.3 编写自己的异常类 ............... 294 10.3.4 嵌套异常(仅限 C++11)......... 297 堆栈的释放与清理 .................... 299 10.4.1 使用智能指针 ....................... 300 10.4.2 捕获、清理并重新抛出 ....... 301 常见的错误处理问题 ................ 301 10.5.1 内存分配错误 ....................... 301 10.5.2 构造函数中的错误 ............... 304 10.5.3 构造函数的 function-try-blocks................. 306 10.5.4 析构函数中的错误 ............... 308 综合应用 .................................... 308 本章小结 .................................... 312 第 11 章 11.1 11.2 深入探讨标准库......................313 编码原则 .................................... 314 11.1.1 使用模板 ............................... 314 11.1.2 使用运算符重载 ................... 317 C++标准库概述 ......................... 317 11.2.1 字符串 ................................... 317 11.2.2 I/O 流..................................... 318 11.2.3 本地化 ................................... 318 11.2.4 智能指针 ............................... 318 11.2.5 异常 ....................................... 318 11.2.6 数学工具 ............................... 319 11.2.7 时间工具(仅限 C++11)......... 319 11.2.8 随机数(仅限 C++11)............. 319 11.2.9 编译时有理数运算 (仅限 C++11) ........................ 319 11.2.10 元组(仅限 C++11)............... 319 11.2.11 正则表达式 (仅限 C++11) ...................... 320 11.2.12 标准模板库 ......................... 320 11.2.13 STL 算法............................. 326 11.2.14 STL 中还缺什么................. 333 11.3 本章小结 ....................................333 第 12 章 12.1 12.2 12.3 12.4 理解容器与迭代器 ................. 335 容器概述 ....................................335 12.1.1 元素的需求 ........................... 336 12.1.2 异常和错误检查 ................... 338 12.1.3 迭代器................................... 338 12.1.4 C++11 的变化....................... 340 顺序容器 ....................................342 12.2.1 vector ..................................... 342 12.2.2 vector特化.................. 359 12.2.3 deque ..................................... 359 12.2.4 list .......................................... 360 12.2.5 array(仅限 C++11) ................ 364 12.2.6 forward_list(仅限 C++11) ..... 364 容器适配器 ................................366 12.3.1 queue ..................................... 366 12.3.2 priority_queue........................ 369 12.3.3 stack....................................... 372 关联容器 ....................................373 12.4.1 pair 工具类............................ 373 12.4.2 map ........................................ 374 12.4.3 multimap................................ 382 12.4.4 set........................................... 385 12.4.5 multiset .................................. 387 XIV 目录 12.5 无序关联容器/哈希表 (仅限 C++11) ............................387 12.5.1 哈希函数 ............................... 387 12.5.2 unordered_map ...................... 388 12.5.3 unordered_multimap .............. 391 12.5.4 unordered_set/unordered_ multiset .................................. 391 12.6 其他容器 .................................... 391 12.6.1 标准 C 风格数组................... 392 12.6.2 string ...................................... 392 12.6.3 流 ........................................... 393 12.6.4 bitset....................................... 393 12.7 本章小结 .................................... 397 第 13 章 13.1 13.2 13.3 13.4 掌握 STL 算法........................399 算法概述 .................................... 399 13.1.1 find 和 find_if 算法 ............... 400 13.1.2 accumulate 算法.................... 402 13.1.3 在算法中使用 C++11 的移动 语义 ....................................... 404 lambda 表达式(仅限 C++11).....404 13.2.1 语法 ....................................... 404 13.2.2 捕捉块 ................................... 406 13.2.3 将 lambda 表达式用作 返回值 ................................... 406 13.2.4 将 lambda 表达式用作参数... 407 13.2.5 示例 ....................................... 408 函数对象 .................................... 410 13.3.1 算术函数对象 ....................... 410 13.3.2 比较函数对象 ....................... 411 13.3.3 逻辑函数对象 ....................... 412 13.3.4 按位函数对象 (仅限 C++11)......................... 412 13.3.5 函数对象适配器 ................... 413 13.3.6 编写自己的函数对象 ........... 419 算法详解 .................................... 420 13.4.1 工具算法 ............................... 421 13.4.2 非修改算法 ........................... 422 13.4.3 修改算法 ............................... 428 13.4.4 排序算法 ............................... 436 13.4.5 集合算法 ............................... 438 13.5 算法示例:审核选民登记 ........440 13.5.1 选民登记审核问题描述 ....... 440 13.5.2 auditVoterRolls 函数 ............. 440 13.5.3 getDuplicates 函数 ................ 441 13.5.4 测试 auditVoterRolls 函数 .... 443 13.6 本章小结 ....................................443 第 14 章 14.1 14.2 14.3 14.4 使用字符串与正则表达式 ...... 445 动态字符串 ................................445 14.1.1 C 风格字符串 ....................... 446 14.1.2 字符串字面量 ....................... 447 14.1.3 C++ string 类......................... 448 14.1.4 原始字符串字面量 (仅限 C++11)......................... 451 本地化 ........................................452 14.2.1 本地化字符串字面量 ........... 452 14.2.2 宽字符................................... 453 14.2.3 非西方字符集 ....................... 453 14.2.4 locale 和 facet........................ 455 正则表达式(仅限 C++11)..........457 14.3.1 ECMAScript 语法................. 458 14.3.2 regex 库 ................................. 463 14.3.3 regex_match() ........................ 464 14.3.4 regex_search()........................ 467 14.3.5 regex_iterator ......................... 468 14.3.6 regex_token_iterator .............. 469 14.3.7 regex_replace() ...................... 472 本章小结 ....................................475 第 15 章 15.1 15.2 15.3 C++ I/O 揭秘.......................... 477 使用流 ........................................477 15.1.1 流的含义 ............................... 478 15.1.2 流的来源和目标 ................... 478 15.1.3 流式输出 ............................... 479 15.1.4 流式输入 ............................... 483 15.1.5 对象的输入输出 ................... 489 字符串流 ....................................491 文件流 ........................................492 XV C++高级编程(第 2 版) 15.3.1 通过 seek()和 tell()在文件中 转移 ....................................... 493 15.3.2 将流连接在一起 ................... 495 15.4 双向 I/O......................................496 15.5 本章小结 .................................... 497 第 16 章 16.1 16.2 16.3 16.4 16.5 16.6 其他库工具.............................499 STD::FUNCTION ...................... 499 有理数 ........................................ 501 Chrono 库...................................503 16.3.1 持续时间 ............................... 503 16.3.2 时钟 ....................................... 507 16.3.3 时点 ....................................... 508 生成随机数 ................................ 509 16.4.1 随机数引擎 ........................... 510 16.4.2 随机数引擎适配器 ............... 512 16.4.3 预定义的引擎和引擎 适配器 ................................... 512 16.4.4 生成随机数 ........................... 513 16.4.5 随机数分布 ........................... 514 元组 ............................................ 517 本章小结 .................................... 520 第 17 章 17.1 17.2 17.3 17.4 自定义和扩展 STL .................521 分配器 ........................................ 521 迭代器适配器 ............................ 522 17.2.1 反向迭代器 ........................... 522 17.2.2 流迭代器 ............................... 524 17.2.3 插入迭代器 ........................... 524 17.2.4 移动迭代器(仅限 C++11)..... 525 扩展 STL....................................527 17.3.1 扩展 STL 的原因.................. 527 17.3.2 编写一个 STL 算法.............. 527 17.3.3 编写一个 STL 容器.............. 530 本章小结 .................................... 564 第Ⅲ部分 掌握 C++的高级特性 第 18 章 18.1 C++运算符重载 ...................... 567 运算符重载概述 ........................ 567 18.1.1 重载运算符的原因 ............... 568 18.1.2 运算符重载的限制 ............... 568 18.1.3 运算符重载的决策 ............... 568 18.1.4 不要重载的运算符 ............... 570 18.1.5 可重载运算符小结 ............... 571 18.1.6 右值引用(仅限 C++11)......... 574 18.2 重载算术运算符 ........................574 18.2.1 重载一元负号和一元正号 ... 574 18.2.2 重载递增和递减运算符 ....... 575 18.3 重载按位运算符和二元逻辑 运算符 ........................................577 18.4 重载插入运算符和提取 运算符 ........................................577 18.5 重载下标运算符 ........................579 18.5.1 通过 operator[]提供只读 访问 ....................................... 582 18.5.2 非整数数组索引 ................... 583 18.6 重载函数调用运算符 ................583 18.7 重载解除引用运算符 ................585 18.7.1 实现 operator*....................... 586 18.7.2 实现 operator-> ..................... 587 18.7.3 operator ->*的含义 ............... 588 18.8 编写转换运算符 ........................588 18.8.1 转换运算符的多义性问题 ... 590 18.8.2 用于布尔表达式的转换 ....... 591 18.9 重载内存分配和释放运算符 ....593 18.9.1 new 和 delete 的工作原理.... 593 18.9.2 重载 operator new 和 operator delete ........................ 595 18.9.3 重载带有额外参数的 operator new 和 operator delete ........................ 597 18.9.4 显式地删除/默认化 operator new 和 operator delete(仅限 C++11) ............... 599 18.10 本章小结 ..................................599 第 19 章 利用模板编写泛型代码.......... 601 19.1 模板概述 ....................................602 19.2 类模板 ........................................602 XVI 目录 19.2.1 编写类模板 ........................... 602 19.2.2 编译器处理模板的原理 ....... 610 19.2.3 将模板代码分布在多个 文件中 ................................... 611 19.2.4 模板参数 ............................... 612 19.2.5 方法模板 ............................... 614 19.2.6 模板类特例化 ....................... 619 19.2.7 子类化模板类 ....................... 622 19.2.8 继承还是特例化 ................... 623 19.2.9 模板别名(仅限 C++11)......... 623 19.2.10 替换函数语法 (仅限 C++11) ...................... 624 19.3 函数模板 .................................... 625 19.3.1 函数模板特例化 ................... 626 19.3.2 函数模板重载 ....................... 627 19.3.3 类模板的 friend 函数模板.... 628 19.4 本章小结 .................................... 629 第 20 章 20.1 20.2 20.3 20.4 20.5 20.6 20.7 模板的高级特性 .....................631 深入了解模板参数 .................... 631 20.1.1 深入了解模板类型参数 ....... 631 20.1.2 模板参数模板介绍 ............... 635 20.1.3 深入了解非类型模板参数 ... 636 模板类部分特例化 .................... 639 通过重载模拟函数部分特 例化............................................ 643 模板递归 .................................... 645 20.4.1 一个 N 维网格:初次尝试 ... 645 20.4.2 一个真正的 N 维网格 .......... 647 类型推导(仅限 C++11) .............652 20.5.1 auto 关键字 ........................... 652 20.5.2 decltype 关键字..................... 653 20.5.3 结合模板使用 auto 和 decltype.................................. 653 可变参数模板(仅限 C++11) .....655 20.6.1 类型安全的可变长度参数 列表 ....................................... 656 20.6.2 可变数目的混入类 ............... 658 元编程 ........................................ 659 20.7.1 编译时阶乘 ........................... 659 20.7.2 循环展开 ............................... 660 20.7.3 打印元组(仅限 C++11)......... 661 20.7.4 类型 trait(仅限 C++11) ......... 663 20.7.5 结论....................................... 668 20.8 本章小结 ....................................668 第 21 章 21.1 21.2 21.3 21.4 21.5 21.6 高效的内存管理 ..................... 669 使用动态内存 ............................669 21.1.1 如何描绘内存 ....................... 670 21.1.2 分配和释放 ........................... 671 21.1.3 数组....................................... 672 21.1.4 使用指针 ............................... 679 数组-指针的对偶性 ................... 681 21.2.1 数组就是指针 ....................... 681 21.2.2 并非所有的指针都是数组 ... 682 低级内存操作 ............................683 21.3.1 指针运算 ............................... 683 21.3.2 自定义内存管理 ................... 684 21.3.3 垃圾回收 ............................... 684 21.3.4 对象池................................... 685 21.3.5 函数指针 ............................... 685 21.3.6 方法和成员的指针 ............... 687 智能指针 ....................................687 21.4.1 旧的过时的 auto_ptr............. 688 21.4.2 新的 C++11 智能指针.......... 688 21.4.3 编写自己的智能指针类 ....... 692 内存常见的陷阱 ........................697 21.5.1 分配不足的字符串 ............... 697 21.5.2 内存泄漏 ............................... 698 21.5.3 双重删除和无效指针 ........... 701 21.5.4 访问内存越界 ....................... 701 本章小结 ....................................702 第 22 章 22.1 22.2 22.3 C++多线程编程 ..................... 703 简介 ............................................703 原子操作库 ................................707 22.2.1 原子类型示例 ....................... 708 22.2.2 原子操作 ............................... 710 线程 ............................................ 711 XVII C++高级编程(第 2 版) 22.3.1 通过函数指针创建线程 ....... 712 22.3.2 通过函数对象创建线程 ....... 714 22.3.3 通过 lambda 创建线程.......... 715 22.3.4 通过成员函数创建线程 ....... 716 22.3.5 线程本地存储 ....................... 717 22.3.6 取消线程 ............................... 717 22.3.7 从线程获得结果 ................... 717 22.3.8 复制和重新抛出异常 ........... 717 22.4 互斥 ............................................ 720 22.4.1 互斥体类 ............................... 720 22.4.2 锁 ........................................... 721 22.4.3 std::call_once ......................... 723 22.4.4 互斥体的用法示例 ............... 724 22.5 条件变量 .................................... 727 22.6 future........................................... 729 22.7 示例:多线程日志记录器类 .... 731 22.8 线程池 ........................................ 736 22.9 线程设计和最佳实践 ................ 737 22.10 本章小结 .................................. 738 第Ⅳ部分 C++软件工程 第 23 章 23.1 23.2 23.3 23.4 23.5 充分利用软件工程方法 ..........741 过程的必要性 ............................ 741 软件生命周期模型 .................... 742 23.2.1 分段模型和瀑布模型 ........... 742 23.2.2 螺旋模型 ............................... 745 23.2.3 Rational 统一过程................. 747 软件工程方法学 ........................ 748 23.3.1 敏捷 ....................................... 748 23.3.2 Scrum..................................... 748 23.3.3 极限编程(XP) ....................... 750 23.3.4 软件分流 ............................... 754 构建自己的过程和方法 ............ 754 23.4.1 对新思想采取开放态度 ....... 754 23.4.2 提出新想法 ........................... 754 23.4.3 知道什么行得通什么行 不通 ....................................... 754 23.4.4 不要逃避 ............................... 755 源代码控制 ................................ 755 23.6 本章小结 ....................................757 第 24 章 24.1 24.2 24.3 24.4 24.5 编写高效的 C++程序 ............. 759 性能和效率概述 ........................759 24.1.1 提升效率的两种方式 ........... 760 24.1.2 两种程序 ............................... 760 24.1.3 C++是不是低效的语言........ 760 语言层次的效率 ........................761 24.2.1 高效地操纵对象 ................... 761 24.2.2 使用内联方法和函数 ........... 765 设计层次的效率 ........................765 24.3.1 尽可能多地缓存 ................... 765 24.3.2 使用对象池 ........................... 766 剖析 ............................................770 24.4.1 使用 gprof 的剖析范例......... 770 24.4.2 使用 Visual C++ 2010 的剖析 范例....................................... 778 本章小结 ....................................780 第 25 章 25.1 25.2 25.3 开发跨平台和跨语言的 应用程序................................ 781 跨平台开发 ................................781 25.1.1 硬件架构问题 ....................... 782 25.1.2 实现问题 ............................... 783 25.1.3 平台相关的特性 ................... 784 跨语言开发 ................................785 25.2.1 混合使用 C 和 C++ .............. 785 25.2.2 转移范例 ............................... 786 25.2.3 和 C 代码链接 ...................... 788 25.2.4 混合使用 C#与 C++ ............. 790 25.2.5 通过 JNI 混合 Java 和 C++................................... 791 25.2.6 混合 C++使用 Perl 和 shell 脚本....................................... 794 25.2.7 混合使用 C++和汇编 代码....................................... 797 本章小结 ....................................798 第 26 章 成为测试专家......................... 799 26.1 质量控制 ....................................800 XVIII 目录 26.1.1 测试是谁的职责 ................... 800 26.1.2 bug 的生命周期 .................... 800 26.1.3 bug 跟踪工具 ........................ 801 26.2 单元测试 .................................... 802 26.2.1 单元测试的方法 ................... 803 26.2.2 单元测试过程 ....................... 803 26.2.3 单元测试实例 ....................... 807 26.3 更高级别的测试 ........................ 813 26.3.1 集成测试 ............................... 813 26.3.2 系统测试 ............................... 815 26.3.3 回归测试 ............................... 815 26.4 成功测试的技巧 ........................ 816 26.5 本章小结 .................................... 816 第 27 章 27.1 27.2 27.3 27.4 27.5 27.6 熟练掌握调试技术 .................. 819 调试的基本定律 ........................ 819 bug 分类学.................................820 避免 bug.....................................820 为 bug 做好规划........................820 27.4.1 错误日志 ............................... 821 27.4.2 调试跟踪 ............................... 822 27.4.3 断言 ....................................... 833 27.4.4 静态断言(仅限 C++11)......... 834 调试技术 .................................... 835 27.5.1 重现 bug................................ 835 27.5.2 调试可重复的 bug ................ 836 27.5.3 调试不可重现的 bug ............ 836 27.5.4 调试内存问题 ....................... 837 27.5.5 调试多线程程序 ................... 841 27.5.6 调试示例:文章引用 ........... 841 27.5.7 从 ArticleCitations 示例中 总结的教训 ........................... 853 本章小结 .................................. 853 第 28 章 28.1 将设计技术和框架结合使用 .... 855 C++编码示例........................... 856 28.1.1 编写一个类 ........................... 856 28.1.2 子类化已有的类 ................... 857 28.1.3 抛出和捕获异常 ................... 858 28.1.4 从文件中读取 ....................... 858 28.1.5 写入文件 ............................... 859 28.1.6 写一个模板类 ....................... 859 28.2 肯定有更好的方法 ....................860 28.2.1 双分派................................... 861 28.2.2 混入类................................... 866 28.3 面向对象的框架 ........................868 28.3.1 使用框架 ............................... 868 28.3.2 模型-视图-控制器范例......... 869 28.4 本章小结 ....................................870 第 29 章 29.1 29.2 29.3 29.4 29.5 29.6 29.7 应用设计模式......................... 871 迭代器模式 ................................872 单实例模式 ................................872 29.2.1 示例:一种日志机制 ........... 873 29.2.2 实现一个单实例 ................... 873 29.2.3 使用一个单实例 ................... 877 29.2.4 单实例模式和多线程 ........... 877 工厂模式 ....................................880 29.3.1 示例:汽车工厂模拟 ........... 880 29.3.2 实现一个工厂 ....................... 882 29.3.3 使用一个工厂 ....................... 884 29.3.4 工厂的其他用途 ................... 885 代理模式 ....................................885 29.4.1 示例:隐藏网络连接的 问题....................................... 885 29.4.2 实现一个代理 ....................... 886 29.4.3 使用代理 ............................... 886 适配器模式 ................................887 29.5.1 示例:适配一个 Logger 类.... 887 29.5.2 实现一个适配器 ................... 888 29.5.3 使用适配器 ........................... 888 装饰器模式 ................................889 29.6.1 示例:在网页中定义 样式....................................... 889 29.6.2 装饰器的实现 ....................... 890 29.6.3 使用一个装饰器 ................... 891 责任链模式 ................................892 29.7.1 示例:事件处理 ................... 892 29.7.2 责任链的实现 ....................... 892 XIX C++高级编程(第 2 版) 29.7.3 责任链的使用 ....................... 893 29.8 观察者模式 ................................ 894 29.8.1 示例:事件处理 ................... 894 29.8.2 观察者的实现 ....................... 894 29.8.3 使用观察者 ........................... 895 29.9 本章小结 .................................... 896 附录 A C++面试................................... 897 附录 B 带注解的参考文献.................... 917 附录 C 标准库头文件........................... 927 XX 第Ⅱ 部分 专业的C++编码方法  第 6 章 熟悉类和对象  第 7 章 掌握类与对象  第 8 章 揭秘继承技术  第 9 章 理解灵活而奇特的 C++  第 10 章 错误处理  第 11 章 深入探讨标准库  第 12 章 理解容器与迭代器  第 13 章 掌握 STL 算法  第 14 章 使用字符串与正则表达式  第 15 章 C++ I/O 揭秘  第 16 章 其他库工具  第 17 章 自定义和扩展 STL 3第 章 面向对象设计 本章内容 ● 什么是面向对象的程序设计 ● 如何定义不同对象之间的关系 ● 抽象的重要性以及如何在设计中使用抽象 在第 2 章,您已经有了对良好软件设计的正确认识,现在可以将对象的概念以及良好设计的概 念组合在一起。在代码中使用对象的程序员与真正掌握了面向对象程序设计的程序员是不同的,后 者能更完美地管理对象相互联系的方式以及程序的总体设计。 本章以面向过程编程到面向对象编程的转换开始。即使您已经有多年使用对象的经验,仍然应 该阅读本章了解关于对象的新思想。本章讨论对象之间的不同关系,包括创建面向对象程序时可能 遇到的陷阱。您还将学习抽象原则如何与对象联系起来。 当从面向过程编程(C-风格)转换到面向对象编程时,需要记住的最重要的一点是,面向对象程 序设计(OOP)只是以不同的方式看待程序。程序员在完全理解什么是对象之前,经常被新的语法以 及 OOP 术语所困惑。本章轻视代码而重视概念以及思想。关于 C++对象的特定语法,可以参阅第 6 章、第 7 章以及第 8 章。 3.1 过程化的思考方式 过程语言(例如 C)将代码分割为小块,每个小块(理论上)完成单一的任务。如果在 C 中没有过程, 所有的代码都会集中在 main()中。您的代码将难于阅读,您的同事会觉得恼火,这还是最轻的。 计算机并不关心代码是位于 main()中还是被分割成具有描述性名称以及注释的小块。过程是一 种抽象,它的存在是为了帮助您、程序员以及阅读或者维护代码的人。这个概念建立在一个与程序 相关的基本问题之上—— 程序究竟做什么?用英语回答这个问题,就是过程化思考。例如,您可以 下面的答案为起点设计一个股票选择程序:首先程序从 Internet 获取股票报价,然后根据特定的指 标将数据排序,之后分析已经排序的数据,最后输出一个建议购买以及出售的列表。当开始编写代 第Ⅰ部分 专业的 C++简介 码的时候,您可能会将脑海中的模型直接转换为 C 函数: retrieveQuotes()、 sortQuotes()、analyzeQuotes()和 outputRecommendations() 尽管 C 将过程表示为“函数”,但 C 并非是一门函数式语言。术语“函数式 (functional)”与“过程(procedural)”有很大的不同,指的是类似于 Lisp 的语言,Lisp 使用的是完全不同的抽象。 当程序遵循特定的步骤序列时,过程方法运行良好。然而,在现代的大型应用程序中,很少有 线性的事件序列,通常用户可以在任何时候执行任何命令。此外,过程思想对于数据的表示没有任 何的说明,在前面的示例中,并没有讨论股票报价实际上是什么。 如果过程模型听起来像您处理程序的方法,不要担心。一旦您意识到 OOP 只是一种更灵活的 替代方法,只是一种对软件的思考方法,面向对象编程就会变得十分自然。 3.2 面向对象思想 与基于“程序做什么”问题的面向过程方法不同,面向对象方法提出另一个问题:模拟哪些实 际对象? OOP 的基本观念是不应该将程序分割为若干任务,而是将其分割为自然对象的模型。乍 看上去这有些抽象,当您用类、组件、属性以及行为等术语考虑实际对象时,这一思想就会变得更 加清晰。 3.2.1 类 类将对象与其定义区分开来。考虑一个桔子(当然您希望是佛罗里达州的柑橘),作为长在树上 美味水果一般概念的桔子以及某个特定桔子(例如正在往我的键盘上滴水的一个桔子)有所不同。 当回答“什么是桔子”的时候,就是在谈论被当作桔子的那一类事物。所有桔子都是水果,所 有桔子都长在树上,所有桔子都是橙色的,所有的桔子都具有特定的味道。类只是简单地封装了用 来定义对象分类的信息。 当描述某个特定的桔子时,实际上是在讨论某个对象。所有的对象都属于某个特定的类。由于 我桌子上的对象是一个桔子,我知道它属于桔子类。因此,我知道它长在树上,还可以进一步指出 它的颜色是中等程度的橙色,并且味道不错。对象是类的一个实例(instance)—— 这个项拥有一些特 征,从而与同一类型的其他项区分开来。 上面的股票选择程序是一个更具体的示例。在 OOP 中,“股票报价”是一个类,因为它定义了 报价的抽象概念。某个特定的报价(例如“当前 Microsoft 股票报价”)是一个对象,因为它是这个类 的特定实例。 如果具有 C 的背景,可以将类以及对象类比为类型以及变量。实际上在第 6 章您将会看到,类 的语法与 C 的结构类似。 3.2.2 组件 如果考虑一个复杂的实际对象,例如飞机,很容易看到它是由许多小的组件(component)组成的。 其中包括机身、控制器、起落装置、引擎以及很多其他部件。对于 OOP 而言,将对象分解为更小 66 第 3 章 面向对象设计 组件是一项必备的能力,就像将复杂任务分解为较小过程是过程化编程的基础一样。 组件与类本质上类似,但组件更小并且更具体。一个优秀的面向对象程序可能具有 Airplane 类, 但是,如果充分描述飞机的话,这个类将会过于庞大。因此 Airplane 类只处理许多小的更容易管理 的组件。每个组件可能还会有更小的组件,例如,起落装置是飞机的一个组件,车轮是起落架的一 个组件。 3.2.3 属性 属性将一个对象与其他对象区分开来。回到 Orange 类,所有桔子都被定义为橙色的并且具有特 定的口味,这两个特征就是属性。所有桔子都具有相同的属性,但属性的值不同。我的桔子味道“可 能不错”,但是您的桔子味道可能“非常难吃”。 可以在类的层次上思考属性。正如前面提到的那样,所有桔子都是水果,都长在树上。这是水 果类的属性,而特定的橙色是由特定的水果对象决定的。类属性被所有的类成员共享,而类的所有 对象体现对象属性,但是具有不同的值。 在股票选择示例中,股票报价具有几个对象属性,包括公司的名称、股票代码以及其他统计 数据。 属性用来描述对象的特征,回答“为什么这个对象与众不同”的问题。 3.2.4 行为 行为回答两个问题:“对象做什么”以及“能对对象做什么”。在桔子示例中,桔子本身不会做 什么,但是我们可以对桔子做一些事情。桔子的行为之一是可以被吃掉。与属性类似,可以在类或 者对象层次上思考行为。几乎所有的桔子都会以相同的方式被吃掉,但是其他行为未必如此,例如 被扔到斜坡上向下滚动,圆桔子与扁圆的桔子的行为明显不同。 前面的股票选择示例提供了一些更为实际的行为。您还记得吧,以过程的方式思考,我们认为 程序的功能之一就是分析股票报价。以 OOP 的方式思考,您可能会认为股票报价对象可以自我分 析,分析变成了股票报价对象的一个行为。 在面向对象编程中,许多功能性的代码从过程转移到对象。通过建立具有某些行为的对象并定 义对象的交互方式,OOP 以更为丰富的机制将代码以及代码操作的数据联系起来。 3.2.5 综合考虑 通过这些概念,可以用另一种方式来看待股票选择程序并以面向对象的方式重新设计这个程序。 在前面说过,“股票报价”类是一个不错的开始。为了获取报价表,程序需要股票报价组的概念, 通常称之为一个集合。因此一个较好设计可能会使用一个类代表“股票报价的集合”,这个类由代表 单个“股票报价”的小组件组成。 再来说属性,这个集合类至少应该有一个属性—— 实际接收到的报价表。可能还具有其他附加 属性,例如最新检索的确切日期以及时间,所获取报价的数目。至于行为,“股票报价的集合”将从 服务器那里获取报价并提供有序的报价表。这就是“获取报价”行为。 股票报价类具有前面已经讨论过的一些属性—— 名称、代码、当前价格等,此外还具有分析行 为。您可能还考虑到其他行为,如买入以及卖出股票。 图示通常有助于呈现组件之间的关系。图 3-1 使用了多行来说明一个“股票报价集合”包含了 67 第Ⅰ部分 专业的 C++简介 多个“股票报价”对象。 图 3-1 当提到用对象表示程序时,列出类的属性以及行为是一种将类可视化的有效方法,如表 3-1 所示。 类 桔子 股票报价集合 股票报价 相关组件 籽 果汁 果肉 表 3-1 属性 颜色 味道 由单个的股票报价对象组成 无(目前为止) 单个报价 时间戳 报价的数目 公司名称 股票代码 当前价格等 行为 吃 滚动 投掷 削 榨汁 获取报价 按照不同标准将报价排序 分析 买入股份 卖出股份 3.3 生活在对象世界里 当程序员的思想从面向过程转换到面向对象模式时,对于将属性以及行为结合到对象,通常会 有一种恍然大悟的感觉。某些程序员重新设计正在执行的项目,并且要将某些部分作为对象重写。 其他程序员可能会试着抛开所有代码并重新开始这个项目,将其作为完全的面向对象应用程序。 使用对象开发程序有两种方法。对于某些人来说,对象只是代表了数据以及功能的良好封装, 这些程序员的程序中大量使用对象,从而使得代码更容易阅读以及维护。采用这种方法的程序员将 独立的代码段切除并且用对象将其替换,就像外科医生植入心脏起搏器那样。这种方法当然没有错, 这些人将对象当作在许多情况下都有益的工具。程序的某些部分(例如股票报价)只是“感觉像一个 对象”。这些部分可以被分离开来,并且用实际的术语描述。 另一些程序员彻底采用 OOP 范例,将一切都转换为对象。在他们心目中,某些对象对应于实 际的事物,例如桔子或者股票报价,而另一些对象封装了更抽象的概念,例如 sorter 或者 undo 对象。 理想的方法或许在这两个极端之间。您的第一个面向对象程序可能实际上只是在传统的过程程序中 使用了几个对象;您也可能全力以赴将所有的事情都作为对象,从表示 int 的类到表示主应用程序 68 第 3 章 面向对象设计 的类。随着时间的推移,您会找到合理的折中方法。 3.3.1 过度使用对象 在设计一个创造性的面向对象系统以及惹恼团队中所有其他人(将所有细小的事情都转换为对 象)之间存在一条细线。正如弗洛伊德过去常说的那样,有时候变量就只是变量。好的,下面解释这 句话的含义。 或许您正在设计一个预计会畅销的井字游戏。您竭尽所能地采用 OOP 方法,因此您坐了下来, 喝一杯咖啡,用一台笔记本大概地描述了所需要的类和对象。在这类游戏中,通常会有一个对象监 视游戏的进度,并判断胜方。为了表示游戏棋盘,您可能想用 Grid 对象跟踪标记以及它们的位置。 实际上,表示 X 或者 O 的 Piece 对象是 Grid 的一个组件。 等下,退回去!这个设计打算用一个类代表 X 或者 O。这就是过度使用对象的一个例子。用 char 不能代表 X 或者 O 么?另外,用一个枚举类型的二维数组表示 Gird 不是更好么?看看表 3-2 建议 的棋子类。 类 Piece 相关组件 无 表 3-2 属性 X 或者 O 行为 无 这个表格有点稀疏,这有力地表明此处的内容很少,并不需要一个对象。 另一方面,深谋远虑的程序员可能这样认为,尽管当前 Piece 类很小,但是使用对象可以在将 来扩展时不受影响。或许发展下去这会成为一个图形程序,用 Piece 类支持绘图行为可能是有用的。 其他属性可能是 Piece 的颜色或者 Piece 是否是最近移动的那个。 另一种方案是考虑方格的状态而不是使用棋子。方格的状态可能是空、X 或者 O。为了在将来 支持图形应用程序,可以设计一个抽象超类 State,其具体子类 StateEmpty、StateX 以及 StateO 知道 如何表示自身。 显然在此不存在正确答案,关键是在设计应用程序时应该考虑这些问题。记住对象是用来帮助 程序员管理代码的,如果只是为了使得代码“更加面向对象”而使用对象,那么就错了。 3.3.2 过于通用的对象 相对于将不应该是对象的事物当作对象,过于通用的对象可能更糟糕。所有的 OOP 学生都以 类似“桔子”的示例开始—— 这确实是对象,不要怀疑。实际的编码中,对象可以非常抽象。许多 OOP 程序都有一个“应用程序对象”,尽管应用程序并不能以物质的形式表现,但是用对象来表示 应用程序仍然是有意义的,因为应用程序本身具有一些属性以及行为。 过于通用的对象是根本不代表具体事物的对象。程序员可能会试图建立一个灵活的或者可以重 用的对象,但是最终得到一个令人迷惑不解的对象。例如,考虑一个管理并显示媒体的程序。这个 程序可以将照片分类,管理数字音乐唱片,还可以作为个人日志。将所有的事物都当作 media 对象 并创建一个可以容纳所有格式的类就是一种过分的做法。这个类可能具有一个名为 data 的属性,这 69 第Ⅰ部分 专业的 C++简介 个属性包含了图像、歌曲或者日志项的原始位,具体取决于媒体的类型;还可能具有一个名为 perform 的行为,可以正确地绘制图像、播放歌曲或者编辑日志项。 这个类过于通用的原因在于属性以及行为的名称。单词 data 本身没有太多的意义——在此必须 使用一个通用词语,因为这个类过度地扩展到三种完全不同的情况。同理,perform 会在三种不同的 情况下执行差别极大的操作。总之,这个设计过于通用,因为 media 不是一个特定的对象;无论在 用户界面中、实际中还是在程序员的头脑中,都不是一个特定的对象。当程序员的脑海中的许多想 法都用一个对象连接起来时,这个类可能就太通用了,如图 3-2 所示。 3.4 对象之间的关系 图 3-2 作为一个程序员,必然会遇到这样的情况:不同的类具有共同的特征,至少看起来彼此有联系。 例如,尽管在一个数字化目录程序中创建一个 media 对象代表图像、音乐以及文本过于通用,但是 这些对象确实有共同的特征。您可能想跟踪它们的最近修改日期以及时间,或者想让它们全部支持 删除行为。 面向对象的语言提供了许多机制来处理对象之间的这种关系。最棘手的问题是理解这些关系实 际上是什么。对象之间的关系主要有两类——“有一个”(has a)关系以及“是一个”(is a)关系。 3.4.1 “有一个”关系 “有一个”关系或者聚合关系的模式是 A 有一个 B,或者 A 包含一个 B。在这种类型的关系中, 可以认为某个对象是另一个对象的一部分。前面定义的组件通常代表着“有一个”关系,因为组件 表示组成其他对象的对象。 动物园以及猴子就是这种关系的一个示例。您可以说动物园有一只猴子,或者动物园包含了一 只猴子。在代码中用 zoo 对象来模拟动物园,这个对象有一个 monkey 组件。 考虑用户界面有助于理解对象之间的关系。尽管并非所有的 UI 都是(尽管现在大多数是)以 OOP 方式实现的,屏幕上的视觉元素也能很好地转换为对象。UI 关于“有一个”关系的类比就是窗口包 含了一个按钮。按钮以及窗口是明显不同的两个对象,但是又明显有某种联系。由于按钮在窗口中, 因此说窗口有一个按钮。 图 3-3 显示了实际的“有一个”关系以及用户界面的“有一个”关系。 70 第 3 章 面向对象设计 窗口有一个按钮 一架飞机有一个机翼(最好是两个) 图 3-3 3.4.2 “是一个”关系(继承) “是一个”关系是面向对象编程的非常基本的概念,因此有许多名称,包括子类(subclass)、扩 展(extending)以及继承(inheriting)。类模拟现实世界包含具有属性以及行为的对象这一事实,继承模 拟这些对象通常以层次方式结合这一事实。“是一个”说明了层次关系。 基本上,“是一个”的模式是 A 是一个 B,或者 A 实际上与 B 非常相似——这可能比较棘手。 再次用简单的动物园为例,但是在此假定除了猴子之外还有其他动物。这句话本身已经建立了关 系—— 猴子是一种动物。与此类似,长颈鹿也是一种动物,袋鼠是一种动物,企鹅也是一种动物。 那又怎么样?当您意识到猴子、长颈鹿、袋鼠以及企鹅具有某种共性时,继承的魔力就出现了。这 些共性就是动物的一般特征。 对于程序员的启示就是,可以定义一个 Animal 类封装所有动物都具有的属性(大小、生活区域、 食物等)以及行为(移动、进食、睡觉)。特定的动物(例如猴子)成为 Animal 的子类,因为猴子包含了 动物的所有特征。记住,猴子是动物,并且还有使它与众不同的其他特征。图 3-4 显示了动物的继 承图示。箭头表明继承关系的方向。 图 3-4 就像猴子与长颈鹿是不同类型的动物一样,用户界面通常也具有不同类型的按钮。例如,复选 框是一个按钮,按钮只是一个可以被点击并执行操作的 UI 元素,Checkbox 通过添加状态(相应的框 是否被选中)扩展了 Button 类。 当类之间具有“是一个”关系时,目标之一就是将常用功能放入超类(superclass),一个可以被 其他类扩展的类。如果您发现所有的子类都具有相似或者完全相同的代码,就应该考虑将一些或者 全部代码放入超类。通过这种方法,所需的改动可以在一个地方完成,将来的子类可以“自由地” 获取这些共享的功能。 71 第Ⅰ部分 专业的 C++简介 1. 继承技术 前面的示例非正式地讲述了继承中使用的一些技术。当生成子类时,程序员有多种方法将某个 对象与其父对象(parent object)或者超类(superclass)区分开来。生成子类有多种方法,生成子类实际 上就是完成语句 A is a B that…的过程。 添加功能 子类可以在超类的基础上添加功能。例如,猴子是一种可以挂在树上的动物。除了具有 Animal 的所有行为之外,Monkey 类还具有在树间游动的行为,这个行为只存在于 Monkey 类。 替换功能 子类可以完全替换或者重写超类的行为。例如,大多数动物都是步行,因此您可能会让 Animal 类拥有一个模拟步行的 move 行为。但是袋鼠是一种通过跳跃而不是步行移动的动物,Animal 超类 的其他属性以及行为仍然适用,Kangaroo 子类只需要替换 move 行为的运行方式。当然,如果您发 现将超类所有的功能都进行了替换,这可能意味着这个子类根本不应该是子类,除非这个超类是一 个抽象超类。抽象超类用作子类的超类,并强迫每个子类实现某个行为,抽象超类无法创建实例, 第 8 章将介绍抽象类。 添加属性 子类还可以向从超类继承来的属性中添加新属性。企鹅具有动物所有的属性,此外还有一个鸟 嘴大小属性。 替换属性 与重写行为的方式类似,C++提供了重写属性的方法。然而,这么做通常是不合适的。不要把 替换属性的概念与子类具有不同属性值的概念混淆。例如,所有动物都具有表明它们吃什么的 diet 属性,猴子吃香蕉,企鹅吃鱼,二者都没有替换 diet 属性——只是赋给属性的值不同而已。 2. 多态性与代码重用 多态性(Polymorphism)指具有标准属性和行为的对象可以互换使用。类定义就像是对象以及与 其交互的代码之间的契约。根据定义,Monkey 对象必须支持 Monkey 类的属性和行为。 这个概念也可以推广到超类。由于所有的猴子都是动物,因此所有的 Monkey 对象都支持 Animal 类的属性和行为。 多态性是面向对象编程的亮点,因为多态性真正地利用了继承所提供的功能。在模拟动物园时, 您可以遍历所有动物园中的动物,并且让每个动物都移动一次。由于所有动物属于 Animal 类,因此 它们知道如何移动。某些动物重写了移动行为,但是这正是亮点所在—— 代码只是告诉每个动物移 动,而不知道也不关心是哪种动物。所有的动物都会以自己的方式移动。 除了多态性之外,使用子类还有另一个原因,通常这只是一个利用现有代码的问题。例如,如 果您需要一个具有回声效果的播放音乐类,而您的同事已经编写了一个播放音乐的类,但是没有任 何其他效果,您可以将这个已有的类扩展,添加新的功能。“是一个”关系仍然适用(回声音乐播放 器是一个增添了回声效果的音乐播放器),但是您不打算互换地使用这些类。最终您得到两个独立的 72 第 3 章 面向对象设计 类,用在程序完全不同的部分(或者用于完全不同的程序),只是为了避免重复的工作二者才有了那 么一点关系。 3.4.3 “有一个”与“是一个”的区别 在现实中,区分对象之间的“有一个”与“是一个”关系相当容易。没人会说桔子有一个水 果—— 桔子是一种水果。在代码中,有时候并不会这么明显。 考虑一个代表哈希表的假想类,哈希表是高效地将键映射到值的一种数据结构。例如,保险公 司使用 Hashtable 类将成员 ID 映射到名称,从而给定一个 ID 就可以方便地找到对应的成员名称。 成员 ID 是键,成员名称是值。 在标准哈希表实现中,每个键都有一个值。如果 ID 14534 映射到名称“Kleper,Scott”,就不 能再映射到成员名称“Kleper,Marni”。在大多数实现中,如果对一个已经有值的键添加第二个值, 第一个值就会消失。换句话说,如果 ID 14534 映射到“Kleper,Scott”,然后又将 ID 14534 分配给 “Kleper,Marni”,那么 Scott 将被遗弃,下面的序列两次调用了假想哈希表 enter()行为,并给出了 每次调用结束后哈希表的内容。hash.enter 用到了超前一点的 C++对象语法,可以将其当作“使用 hash 对象的 enter 行为”。 hash.enter(14534, "Kleper, Scott"); 键 14534 值 "Kleper, Scott" [字符串] hash.enter(14534, "Kleper, Marni"); 键 14534 值 "Kleper, Marni" [字符串] 不难想象类似于哈希表但是允许一个键有多个值的数据结构的使用。在保险公司示例中,一个 家庭可能有多个名称对应于同一个 ID。由于这种数据结构非常类似于哈希表,因此可以用某种方式 使用哈希表的功能。哈希表的键只能有一个值,但是这个值是任意类型。除了字符串之外,这个值 还可以是一个包含多个键值的集合(例如数组或者列表)。当向已有 ID 添加新的成员时,可以将名称 加入到集合中。运行方式如下所示: Collection collection; // Make a new collection. collection.insert("Kleper, Scott"); // Add a new element to the collection. hash.enter(14534, collection); // Enter the collection into the table. 键 14534 值 {“Kleper, Scott”}[集合] Collection collection = hash.get(14534);// Retrieve the existing collection. collection.insert(“Kleper, Marni”); // Add a new element to the collection. hash.enter(14534, collection); // Replace the collection with the updated one. 73 第Ⅰ部分 专业的 C++简介 键 14534 值 {“Kleper, Scott”, “Kleper, Marni”} [集合] 使用集合而不是字符串有些繁琐,并且需要大量重复代码。在一个单独的类中封装多值功能应 该会比较好,可以将这个类叫做 MultiHash。MultiHash 类的运行与 Hashtable 类似,只是暗地里将 每个值作为字符串的集合而不是单个字符串存储。很明显,MultiHash 与 Hashtable 有某种联系,因 为它仍然使用哈希表存储数据。不明显的是,这是“是一个”关系还是“有一个”关系。 先考虑“是一个”关系。假定 MultiHash 是 Hashtable 的子类,它必须重写在表中添加项的行为, 从而既可以创建集合并添加新的元素,又可以获取已有集合并添加新的元素。此外还必须重写获取 值的行为。例如,可以将给定键的所有值集中到一个字符串。这好像是一个相当合理的设计。即使 子类重写了超类所有的行为,仍然可以在子类中使用原始行为,从而使用超类的行为。这种方法如 图 3-5 所示。 图 3-5 现在考虑“有一个”关系。MultiHash 属于自己的类,但是包含了 Hashtable 对象。这个类的接 口可能与 Hashtable 非常相似,但是并不需要相同。在幕后,当用户向 MultiHash 添加项时,会将这 个项封装到一个集合并送入 Hashtable 对象。这也很合理,如图 3-6 所示。 图 3-6 那么,哪个方案是正确的?没有明确的答案,本书的作者之一认为这是“有一个”关系,他编 写了一个 MultiHash 类供产品使用。主要原因是允许修改公开的接口而不需要担心维护哈希表的功 能。例如,图 3-6 中 get 行为变成了 getAll,清楚表明将获取 MultiHash 中某个特定键所有的值。此 外,在“有一个”关系中,不需要担心哈希表功能会渗透。例如,如果哈希表类提供了获取值的总 数的方法,只要 MultiHash 不重写这个方法,就可以用这个方法报告集合的数目。 这就是说,MultiHash 实际上是一个具有新功能的 Hashtable 这一说法是让人信服的,因此应该 是“是一个”关系。关键在于有时候这两种关系之间的差别很小,您需要考虑使用类的方式,还需 要考虑您创建的类只是利用了其他类的一些功能,还是在其他类的基础上修改或者添加新功能。 74 第 3 章 面向对象设计 表 3-3 给出了关于 MultiHash 两种方法的支持以及反对意见。 支持的原因 反对的原因 是一个 表 3-3 ● 基本上,这是具有不同特征的同一抽象 ● 这个类的行为与 Hashtable(几乎)相同 ● 根据定义,哈希表一个键对应一个值,将 MultiHash 当作哈希表是错误的 ● MultiHash 将哈希表的两个行为全部重写,这 有力地说明这个设计是错误的 ● Hashtable 未知的或者不正确的属性以及行为 会“渗透”到 MultiHash 有一个 ● MultiHash 可以拥有任何有用的行为,而不 需要考虑 Hashtable 拥有什么行为 ● 可以不采用 Hashtable 实现方式,同时不需 要改变公开的行为 ● 在某种意义上,MultiHash 通过提出新行为 进行了重造 ● Hashtable 的一些其他属性以及行为可能是 有用的 反对“是一个”关系的理由在这种情况下非常有力。实际上,根据作者多年的经验,如果要选 择的话建议采用“有一个”关系而不是“是一个”关系。 注意,在这里使用 Hashtable 和 MultiHash 说明了“有一个”和“是一个”关系的不同。在代码 中,建议使用标准哈希表类而不是自己写一个。C++11 的标准库中提供了一个 unordered_map 类, 用来代替 Hashtable,此外还提供了一个 unordered_multimap 类,可以用来代替 MultiHash 类。在第 12 章将讨论这两个标准类。 3.4.4 Not-a 关系 当考虑类之间的关系时,应该考虑类之间是否真的存在关系。不要把对面向对象设计的热情全 部转换为许多不必要的类/子类关系。 当实际事物之间存在明显关系而代码中没有实际关系的时候,问题就出现了。OO 层次结构需 要模拟功能关系而不是人为制造关系。图 3-7 显示的关系作为概念集或者层次结构是有意义的,但 是在代码中并不能代表有意义的关系。 图 3-7 75 第Ⅰ部分 专业的 C++简介 避免不必要子类的最好方法是首先给出一个大概的设计。为每个类和子类写出计划设置的属性 和行为。如果发现某个类没有自己特定的属性或者行为,或者某个类的所有属性和行为都被子类重 写,只要这个类不是前面提到过的抽象超类,就应该重新考虑您的设计。 3.4.5 层次结构 正如类 A 可以是 B 的超类一样,B 也可以是 C 的超类。面向对象层次结构可以模拟类似的多 层关系。一个具有多种动物的动物园模拟程序,可能会将每种动物作为 Animal 类的子类,如图 3-8 所示。 图 3-8 当编写每个子类的代码时,您可能会发现许多代码是相似的。当出现这种情况时,应该考虑给 它们一个共同的超类。Lion 和 Panther 的移动方式以及食物相同,说明可以使用一个 BigCat 类。还 可以进一步将 Animal 类细分以包括 WaterAnimals 和 Marsupials。图 3-9 显示了利用了这种共性的更 加系统化的设计。 图 3-9 生物学家看到这个体系可能会失望——海豚和企鹅并不属于同一科。然而,这强调了一个要 点—— 在代码中,需要平衡现实关系以及共享功能关系。即使现实中两种事物紧密联系,在代码中 可能没有任何关系,因为它们没有共享功能。您可以简单地把动物区分为哺乳动物以及鱼类,但是 这种做法会使得超类没有任何共同因素。 另一个要点是可以用其他方法创建这个层次结构。前面的设计基本上是根据动物的移动方式创 建的。如果根据动物的食物或者身高创建,这个层次结构可能会有很大的不同。总之,关键在于如 何使用类,需求决定对象层次结构的设计。 优秀的面向对象层次结构能够做到以下几点: ● 使得类之间存在有意义的功能关系。 ● 将共同的功能放到超类,从而支持代码重用 ● 避免子类过多地重写超类的功能,除非超类是一个抽象类。 76 第 3 章 面向对象设计 3.4.6 多重继承 到目前为止,所有示例都只有单一的继承链。换句话说,对于一个给定的类最多只有一个直接 的超类。这不是必须的,在多重继承中,一个类可以有多个超类。 图 3-10 给出了一个多重继承设计。在此仍然有一个名为 Animal 的超类,根据大小将这个类进 一步划分。此外根据食物划分了一个独立的层次类别,考虑移动方式又划分了一个层次类别。所有 类型的动物都是这三个类的子类,如下所示。 图 3-10 考虑用户界面环境,假定用户可以点击某张图片。这个对象好像既是按钮又是图片,因此其实 现同时涉及了 Image 类和 Button 类的派生,如图 3-11 所示。 图 3-11 在某些情况下多重继承可能很有用,但是必须记住它也有很多缺点。许多程序员不喜欢多重继 承,C++明确支持这种关系,而 Java 语言根本不予支持。批评多重继承是有原因的。 首先,用图形来表示多重继承十分复杂。如图 3-10 所示,当存在多重继承以及交叉线的时候, 即使简单的类层次也会变得非常复杂。类层次结构的目的是让程序员更方便地理解代码之间的关系。 而在多重继承中,类可以有多个彼此没有关系的超类。这么多类加入到对象的代码中,您真的能够 跟踪发生了什么吗? 其次,多重继承能够破坏清晰的层次结构。在动物示例中,使用多重继承方法意味着 Animal 超类的作用降低,因为描述动物的代码现在被分成了三个独立的层次。尽管图 3-10 中的设计显示了 三个清晰的层次,但是不难想象它们会变得如何凌乱。例如,如果您意识到所有的 Jumper 不仅以同 样的方式移动,还吃同样的食物,该怎么办?由于层次是独立的,因此没有办法在不添加其他超类 的情况下加入移动以及食物的概念。 第三,多重继承的实现很复杂。如果两个超类以不同方式实现了相同的行为,该怎么办?两个 超类本身是同一个超类的子类,可以这样么?这种可能让实现变得复杂,因为在代码中建立这样复 杂的关系对于作者以及读者而言都很困难。 其他语言取消多重继承的原因是通常应该避免使用多重继承。通过重新考虑层次或者使用第 29 章讲述的设计模式,在控制某个项目的设计时通常可以避免引入多重继承。 77 第Ⅰ部分 专业的 C++简介 3.4.7 混入类 混入(mix-in)类代表类之间的另一种关系。在 C++中,混入类的语法类似于多重继承,但是语义 完全不同。混入类回答“这个类还可以做什么”这个问题,答案经常以“-able”结尾。通过混入类, 可以向类中添加功能而不需要保证完全的“是一个”关系。您可以把它当作一种分享(share-with) 关系。 回到动物园示例,您可能想引入某些动物是可以“做宠物”这一概念。也就是说,有些动物可 能不需要训练就可以作为动物园游客的宠物。您可能想让所有可以做宠物的动物支持“做宠物”行 为。由于可以做宠物的动物没有其他的共性,因此您不想破坏已经设计好的层次结构,Pettable 就是 很好的混入类。 混入类经常在用户界面中使用。您可以说 Image 能够点击,而不需要说 PictureButton 类既是 Image 又是 Button。您桌面上的文件夹图标可以是一个可以拖动的 Image。软件开发人员总是喜欢弄 一大堆有趣的形容词。 当考虑类的差异而不是代码的差异时,混入类以及超类的区别还有很多。因为范围有限,混入 类通常比多重层次结构容易理解。Pettable 混入类只是在已有类中添加了一个行为,Clickable 混入类 或许仅仅添加了“按下鼠标”以及“抬起鼠标”行为。此外,混入类很少会有庞大的层次结构,因 此不会出现功能的交叉混乱。 3.5 抽象 在第 2 章中讲述了抽象的概念——将实现与访问方式分离的概念。前面说过,抽象是一种优秀 的思想,同时还是面向对象设计的基础。 3.5.1 接口与实现 抽象的关键在于有效分离接口与实现。实现是用来完成任务的代码,接口是其他用户使用您的 代码的方式。在 C 中,描述库函数的头文件是接口,在面向对象编程中,类的接口是公有属性以及 行为的集合。优秀的接口只包含公有行为,类的属性/变量绝不应该是公有,但是可以通过公有行为 公开,这些行为叫做 getter 以及 setter。 3.5.2 决定公开的接口 当设计类的时候,其他程序员如何与您的对象交互是一个问题。在 C++中,类的属性以及行为 可以是公有的(public)、保护的(protected)以及私有的(private)。将属性或者行为设置为 public 意味着 其他代码可以访问它们。protected 意味着其他代码不能访问这个属性或者行为,但是子类可以访问。 private 是最严格的控制,意味着不仅其他代码不能访问这个属性或者行为,子类也不能访问。 设计公开的接口就是选择哪些接口应该成为 public。当与其他程序员一起完成一个大项目时, 应该将设计公开接口作为一个步骤。 1. 考虑用户 设计公开接口的第一步是考虑为谁设计。用户是团队中的其他成员吗?这个接口只是您个人使 78 第 3 章 面向对象设计 用吗?公司外面的程序员会使用接口吗?是某个用户还是国外的承包商?除了判断谁会用到接口之 外,还应该注意您的设计目标。 如果接口供自己使用,那么设计起来会更加灵活,为了适合您的需要可以改变它。然而,应该 记住团队中角色会改变,很有可能在某一天其他人也会用这个接口。 设计供其他程序员使用的接口有一点不同之处。在某种意义上,接口变成了您与他们之间的契 约。例如,如果您实现程序的数据存储组件,其他人依靠这个接口支持某些操作。您应该找出团队 中其他成员需要您的类完成的所有工作。他们需要版本控制吗?可以存储什么类型的数据?作为契 约,您应该把接口看成是几乎不可改变的。如果在开始编码之前就接口达成了一致,而您在开始编 码之后改变了接口,就会听到许多抱怨声。 如果用户是外部客户,对设计有不同的要求。理想情况下,目标客户会参与指定接口公开的功 能。您应该同时考虑用户需要的特定功能以及他们在将来可能会要求的功能。接口中使用的术语必 须是客户熟悉的,并且必须为这些客户编写文档。设计中不应该出现内部的笑话、代号以及程序员 的俚语。 2. 考虑目的 编写接口有很多理由。在编写代码之前,甚至在决定公开的功能之前,必须理解接口的目的。 应用程序编程接口(API) API 是一种外部可见机制,用于在其他环境中扩展产品或者使用其功能。如果说内部的接口是 契约,那么 API 更接近于刻在石头上的法律。一旦用户开始使用您的 API,哪怕他们不是您所在公 司的员工,他们也不希望 API 改变,除非是加入帮助他们的新功能。在交给用户使用之前,应该关 心 API 的设计并与用户进行商谈。 设计 API 时主要考虑使用的难度以及灵活性。由于接口的目标用户并不熟悉产品内部的运行方 式,因此学习使用 API 是一个逐步的过程。毕竟,公司向用户公开这些 API 的目的是想让用户使用 API。如果使用难度太大,API 就是失败的。灵活性经常与此对立,您的产品可能有许多不同的用途, 您想让用户使用提供的所有功能。然而,如果一个 API 让用户做您产品可做的任何事,会过于复杂。 正如编程格言所说的那样“好的 API 使得容易的情况变得更容易,艰难的情况变得可能”。也 就是说,API 应该容易被使用。大多数程序员想要做的事情就是访问。然而,API 应该允许更高级 的用法,因此在少见的复杂情况以及常见的简单情况之间的折中是可以接受的。 工具类或者库 通常,您的任务是设计某些特定的功能供应用程序中的其他部分使用,可能是一个随机数库或 者一个日志类。在此情况下比较容易确定接口,因为您倾向于公开大多数或者全部功能,理想情况 不应该给出与实现有关的内容。通用性是需要考虑的重要问题,由于类或者库是通用的,因此在设 计中应该考虑设置用例。 子系统接口 您可能设计程序中两个主要子系统之间的接口,例如访问数据库的机制。在此情况下,将接口 与实现分离异常重要,其他程序员可能会在您的实现完成之前依靠您的接口编写他们的实现。当所 做的事情与子系统相关时,首先考虑子系统的主要目的是什么。一旦定义了子系统的主要任务,就 79 第Ⅰ部分 专业的 C++简介 可以考虑子系统具体的用法以及如何将它展示给代码的其他部分。试着从他人的角度考虑问题,而 不要陷入实现的细节。 组件接口 您定义的大多数组件接口可能都小于子系统接口或者 API,组件是您在其他代码中会用到的对 象。在这些情况下,当接口逐渐增大并且变得难以控制时就可能会出现问题。哪怕这些接口是供您 自己使用的,也要当成不是。与子系统接口类似,此时应该考虑每个类的主要目的,并且不要公开 对这个目的没有贡献的功能。 考虑将来 在设计接口的时候,应该考虑将来的需求。您会在这个设计上花费数年的时间么?如果是这样, 您可能需要使用插件架构,从而留出扩展空间。您能够确定人们使用接口的目的与当初设计的目的 相同么?与他们交流,更好地理解他们的使用情况。否则以后就要重写接口,或者更糟糕的是,以 后可能需要不时地添加新的功能从而使接口变得凌乱不堪。要小心!推测性的通用是另一个陷阱。 如果将来的用途不明,不要设计包含一切的日志类,因为这样做会不必要地将设计、实现以及公有 接口复杂化。 3.5.3 设计成功的抽象 经验以及重复是良好抽象的基础。只有经过多年的编写代码以及使用抽象,才能真正地设计良 好的接口。当遇到其他抽象的时候,试着记住什么可行,什么不可行。您发现上周使用的 Windows 文件系统 API 有什么缺陷?如果您编写网络软件包,与您同事编写的有什么不同?最好的接口往往 并不是一次就能得到的,因此要反复尝试。把您的设计交给同行征求他们的意见。如果您的公司有 代码评审,可以将检查接口规范作为评审代码的开始,此后再开始实现。在开始编码后也不要惧怕 修改抽象,哪怕这样做意味着强迫其他程序员进行改动,他们会意识到一个良好的抽象可以让每个 人在很长一段时间内受益。 有时候您跟其他程序员交流自己的设计时应该传播点好消息。或许团队的其他成员没有意识到 前面设计的问题,或者他们觉得按照您的方法工作量太大。在此类情况下,要准备好保护您工作并 在适当的时候与他们进行沟通。 良好的抽象意味着接口只有公有行为。所有代码都应该在实现文件而不是类定义文件中。这意 味着包含类定义的接口文件是稳定的,不会改变。 小心单一类的抽象。如果您编写的代码非常深奥,应该考虑用其他类配合主接口。例如,如果 您公开一个完成数据处理的接口,还要考虑编写一个结果对象,从而提供一种简单的方法查看并说 明结果。 当可能的时候,将属性转换为行为。换句话说,不要让外部代码直接操作类的数据。您不想让 一些粗心的或者恶意的程序员把兔子对象的高度设置为负数,为此可以让“设置高度”行为进行边 界检查。 值得再次一提的是重复,因为这非常重要。应该查找并回应设计中的缺陷,在必要的时候进行 修改,并从错误中获取教训。 80 第 3 章 面向对象设计 3.6 本章小结 在这一章讲述了面向对象程序的设计而没有给出太多的代码。本章的概念几乎对于全部的面向 对象语言都适用。有些内容您可能已经知道,有些内容是用新的方法阐述您已经熟悉的概念。或许 您会找到解决旧问题的新方法,或者对于您一直在团队中宣讲的概念,本章提供了有利的新论据。 即使您从来没有使用过或者只是少量使用过对象,现在您对设计面向对象程序相关知识的了解不比 经验丰富的 C++程序员少。 要重点关注对象之间的关系,因为组织良好的对象不仅有助于代码重用并减少混乱,还因为您 可能在团队中工作。以有意义的方式联系在一起的对象易于阅读和维护。当设计程序的时候,可以 参阅 3.4 节。 最后,您学习了与创建成功的抽象的相关知识,以及设计时需要重点考虑的两个因素—— 用户 以及目的。 下一章继续讨论设计主题,介绍如何设计可重用代码。 81 5第 章 编码风格 本章内容 ● 编写代码文档的重要性以及可以使用的注释风格 ● 分解(decomposition)的含义以及用法 ● 什么是命名约定(naming conventions) ● 什么是格式规则(formatting rules) 如果您每天花费数个小时使用键盘编写代码,您应该为您的工作感到骄傲,编写可以完成任务 的代码只是程序员全部工作的一部分而已。任何人都可以学习编写基本的代码,编写具有风格的代 码才算是真正掌握了编码。 本章讲述如何编写优秀的代码,在本章您将会看到几种 C++风格。您会发现简单地改变代码的 风格可以极大地改变代码的表现。例如,Windows 程序员编写的 C++代码通常具有自己的风格,使 用了 Windows 的习惯。Mac OS 程序员编写的 C++代码与之相比几乎是完全不同的语言。如果打开 的 C++源代码一点都不像您了解的 C++时,接触几种不同的风格可以帮助您避免这种消沉的感觉。 5.1 良好外观的重要性 编写文体上“良好”的代码很费时间。您或许在几个小时内就可以匆匆写出解析 XML 文件的 程序。而编写功能分离、注释充分、结构清晰的同样一个程序可能要花费数天的时间。这么做值 得吗? 5.1.1 事先考虑 如果一个新的程序员在一年之后不得不使用您的代码,您对代码有多少信心?本书的作者之一 面对日益混乱的网络应用程序代码,让他的团队假想一个一年后加入的实习生。如果没有文档而函 数有好几页长,这个可怜的实习生如何才能赶上代码的进度?当编写代码的时候,可以假定某个新 人在将来不得不维护这些代码。您还记得代码如何运行吗?如果您不能提供帮助会怎么样?良好的 第Ⅰ部分 专业的 C++简介 代码由于易于阅读和理解,因此不存在这些问题。 5.1.2 良好风格的元素 很难列举 “文体良好”代码具有的特征。随着时间的推移,您会发现您喜欢的风格,并从他人 编写的代码中找到有用的技巧。或许更为重要的是,您遇到的可怕代码可以教会您应该避免什么样 的风格。当然,良好的代码有一些共同的原则,本章将就此进行讨论。 ● 文档 ● 分解 ● 命名 ● 语言的使用 ● 格式 5.2 为代码编写文档 在编程环境下,文档通常指源文件中的注释。当编写相关代码的时候,注释用来说明您当时的 想法。在这里给出的信息很难通过阅读代码获取。 5.2.1 使用注释的原因 很明显使用注释是一个好主意,但是您想过为什么代码需要注释么?有时候程序员意识到注释 的重要性,但是没有完全理解为什么注释如此重要。本章将解释使用注释的全部原因。 1. 说明用途的注释 使用注释的原因之一是说明客户如何与代码交互。头文件中每个公有访问的函数或者方法都应 该带有解释其行为的注释。某些组织喜欢将注释正式化,明确列出每有方法的目的、方法的参数、 方法的返回值以及可能抛出的异常。 为公有方法提供注释完成了两件事情。首先,注释让您获得了一个机会,可以用英语陈述在代 码中无法陈述的内容。例如,在 C++中确实没有办法说明数据库对象的 saveRecord()方法只能在 openDatabase()方法之后调用。但可以使用注释提示这一限制,如下所示: /* * saveRecord() * * Saves the given record to the database. * * This method will throw a "DatabaseNotOpenedException" * if the openDatabase() method was not called first. */ 公有方法注释的第二个效果是可以说明用法。C++语言强迫您指定方法的返回类型,但是您无 法说明返回值实际代表了什么。例如,saveRecord()方法的声明可能指出这个方法返回一个 int,但 是阅读这个声明的客户无法得知 int 的含义。在注释中可以加入其他辅助数据,如下所示: 96 第5章 编 码 风 格 /* * saveRecord() * * Saves the given record to the database. * * Parameters: * Record& rec: the record to save to the database. * Returns: int * An integer representing the ID of the saved record. * Throws: * DatabaseNotOpenedException if the openDatabase() method was not * called first. */ 有时函数的参数以及返回值是泛型,可以用来传递任何类型的信息。在此情况下应该清楚地用 文档说明所传递的确切类型。例如,Windows 的消息处理程序接受两个参数,LPARAM 以及 WPARAM,返回一个 LRESULT。这些参数以及返回值可以传递您喜欢的任何内容,但是您不能改 变它们的类型。通过使用类型转换,可以用它们传递简单的整数,或者传递指向某个对象的指针。 文档应该是这样的: * Parameters: * WPARAM wParam: (WPARAM)(int): An integer representing an ID. * LPARAM lParam: (LPARAM)(string*): A string representing... * Returns: (LRESULT)(Record*) * A pointer to a Record object or nullptr in case of an error. 大多数编辑器允许绑定按键以执行某种操作。您可以绑定某个按键,从而使得编辑器自动插入 一个标准注释块,然后您可以在其中填入正确的信息。例如,可以自动插入下面的注释模板。 /* * func() * * Description of the function. * * Parameters: * int param1: parameter 1. * Returns: int * An integer representing... * Throws: * Exception1 if... * Notes: * Additional notes... */ 2. 用来说明复杂代码的注释 在实际的源代码中,好的注释同样重要。在一个处理用户输入并将结果输出到控制台的简单程 序中,阅读并理解所有代码可能很容易。然而在专业领域,代码的算法经常会非常复杂或者深奥, 从而很难理解。 考虑下面的代码。这段代码写得很好,但是可能无法一眼就看出其作用。如果以前见过这个算 法您可能会认出它来,但是新人可能无法理解代码的运行方式。 97 第Ⅰ部分 专业的 C++简介 void sort(int inArray[], int inSize) { for (int i = 1; i < inSize; i++) { int element = inArray[i]; int j = i – 1; while (j >= 0 && inArray[j] > element) { inArray[j+1] = inArray[j]; j--; } inArray[j+1] = element; } } 较好的做法是使用注释描述所使用的算法。下面是改良后的函数,顶部的注释在较高层次说明 了这个算法,行内的注释解释了可能令人疑惑的特定行。 /* * Implements the "insertion sort" algorithm. The algorithm separates the * array into two parts--the sorted part and the unsorted part. Each * element, starting at position 1, is examined. Everything earlier in the * array is in the sorted part, so the algorithm shifts each element over * until the correct position is found for the current element. When the * algorithm finishes with the last element, the entire array is sorted. */ void sort(int inArray[], int inSize) { // Start at position 1 and examine each element. for (int i = 1; i < inSize; i++) { int element = inArray[i]; // j marks the position in the sorted part of the array. int j = i – 1; // As long as the current slot in the sorted array is higher than // the element, shift the slot over and move backwards. while (j >= 0 && inArray[j] > element) { inArray[j+1] = inArray[j]; j--; } // At this point the current position in the sorted array // is *not* greater than the element, so this is its new position. inArray[j+1] = element; } } 新代码长度有所增加,但是通过注释,不熟悉排序算法的读者可能也会理解这个算法。某些组 织中不赞成使用行内注释。在此类情况下,编写整洁的代码并在顶部给出函数的注释变得至关重要。 3. 传递元信息的注释 使用注释的另一个原因是在高于代码的层次提供信息。元信息提供创建代码的详细信息,但是 不涉及代码的特定行为。例如,您所在的组织可能想要使用元信息跟踪每个方法的原始作者。您还 可以使用元信息引用外部文档者其他代码。 下面的示例给出了元信息的几个实例,包括文件的作者,创建日期,提供的特性。此外还包括 98 第5章 编 码 风 格 了表示元数据的行内注释,例如对应于某行代码的 bug 编号,还有注释提醒以后重新访问代码中可 能存在的某个问题。 /* * Author: marcg * Date: 110412 * Feature: PRD version 3, Feature 5.10 */ int saveRecord(Record& rec) { if (!bDatabaseOpen) { throw DatabaseNotOpenedException(); } int id = getDB()->saveRecord(rec); if (id == -1) return -1; // Added to address bug #142 – jsmith 110428 rec.setId(id); // TODO: What if setId() throws an exception? – akshayr 110501 return id } 在每个文件的开头还可以包含修改日志。下面给出了一个修改日志的示例。 /* * Date | Change *----------+-------------------------------------------------* 110413 | REQ #005: Do not normalize to maximum * | value if values > 32767. * 110417 | REQ #006: use nullptr instead of NULL. */ 然而,如果您使用第 23 章讲述的源代码控制方案,就不需要这么做。所有的源代码控制方案(例 如 CVS)都支持签入注释(check-in comments),您应该分别签入每个修改请求。例如,如果您需要在 一个文件中处理两个修改请求,应该签出(check-out)文件,处理第一个修改请求,然后签入文件并 给出适当的修改日志注释。之后才再次能签出文件并处理第二个修改请求。如果您想要同时处理两 个修改请求,可以将源文件分成两个分支,然后在一个分支中处理第一个修改请求,在第二个分支 中处理第二个修改请求。当执行结束时,可以将两个分支合并,每个分支都有恰当的修改日志注释。 通过这种方法,您不需要在每个文件的开头手动维持修改日志。 注释很容易走向极端。最好与您的团队讨论那种类型的注释最有用并制定一个方针。例如,如 果团队的某个成员使用“TODO”注释表明代码仍然需要加工,但是其他人不知道这个约定,这段 危险的代码可能就会被忽略。 如果团队决定使用元信息注释,那么所有人都要包含相同的信息,否则文件会不 一致。 5.2.2 注释的风格 每个组织注释代码的方法都不同。在某些环境中,为了让代码文档具有统一标准,需要使用特 99 第Ⅰ部分 专业的 C++简介 定的风格。在其他环境中,注释的数量以及风格由程序员决定。下面的示例给出了注释代码的几种 方法。 1. 每行都加入注释 避免缺少文档的方法之一是在每行都包含一条注释。每行代码都加入注释可以保证您编写的所 有内容都具有特定的理由。但在实际中,如果代码非常多,这么多的注释是混乱、繁琐而无法做到 的。例如,考虑下面没有意义的注释: int result; result = doodad.getResult(); if (result % 2 == 0) { logError(); } else { logSuccess(); } return result; // Declare an integer to hold the result. // Get the doodad's result. // If the result mod 2 is 0 ... // then log an error, // otherwise ... // log success. // End if/else // Return the result 代码中的注释好像把每行代码当成容易阅读的英语故事讲述。如果读者具有基本的 C++技能, 这完全没有用。这些注释没有引入任何的代码附加信息。看一下这行: if (result % 2 == 0) { // If the result mod 2 is 0 ... 这行注释只是将代码翻译成了英语,并没有说明为什么程序员用 2 对结果求模。较好的注释应 该是这样的: if (result % 2 == 0) { // If the result is even ... 修改后的注释给出了代码的附加信息,尽管对于大多数程序员而言这非常明显。结果对 2 求模 是因为代码需要检测结果是否是偶数。 尽管注释太多会有冗长以及多余的倾向,但是当代码很难理解时这样做还是有必要的。下面的 代码也是每行都有注释,但是这些注释确实有用。 // Call the calculate method with the default values. result = doodad.calculate(getDefaultStart(), getDefaultEnd(), getDefaultOffset()); // To determine success or failure, we need to bitwise AND the result with // the processor-specific mask (see "Doodad API v1.6", page 201). result &= getProcessorMask(); // Set the user field value based on the "Marigold Formula. " // (see "Doodad API v1.6", page 136) setUserField((result + kMarigoldOffset) / MarigoldConstant) + MarigoldConstant); 这段代码的环境不明,但是注释可以让您理解每行代码的作用。如果没有注释,很难解释与& 以及神秘的“Marigold Formula”相关的计算。 通常没必要给每行代码都添加注释,但是如果代码非常复杂因而需要这样做的时 候,不要只是将代码翻译成英语:要解释代码实际上在做什么。 100 第5章 编 码 风 格 2. 前置注释 您的团队可能决定所有的源文件都使用标准注释。这是记录程序以及特定文件重要信息的好机 会。您可能想在每个文件的顶部加入说明信息,下面给出了一些示例信息: ● 最近的修改日期 ● 原始作者 ● 前面所讲的修改日志 ● 文件给出的功能 ID ● 版权信息 ● 文件或者类的简要说明 ● 未完成的功能 ● 已知的 bug 您的开发环境可能允许创建模板,从而自动启动具有前置注释的新文件。某些源代码控制系统 (例如并发版本系统,CVS)甚至可以帮助填写元数据。例如,如果您的注释包含了字符串$Id$,CVS 将自动扩展注释,包含作者、文件名、版本以及日期。下面的示例给出了一个前置注释: /* * $Id: Watermelon.cpp,v 1.6 2004/03/10 12:52:33 klep Exp $ * * Implements the basic functionality of a watermelon. All units are expressed * in terms of seeds per cubic centimeter. Watermelon theory is based on the * white paper "Algorithms for Watermelon Processing." * * The following code is (c) copyright 2011, FruitSoft, Inc. ALL RIGHTS RESERVED */ 3. 固定格式的注释 以标准格式编写可以被外部文档生成器解析的注释是一种日益流行的编程方法。在 Java 语言 中,程序员可以用标准格式编写注释,从而允许名为 JavaDoc 的工具自动为项目创建超文本文档。 对于 C++而言,名为 Doxygen(www.doxygen.org)的免费工具可以解析注释,从而自动生成 HTML 文档、类图、UNIX man pages 以及其他有用的文档。Doxygen 甚至可以辨别并解析 C++程序中 JavaDoc 格式的注释。下面的代码给出了可以被 Doxygen 识别的 JavaDoc 格式的注释。 /** * Implements the basic functionality of a watermelon * TODO: Implement updated algorithms! */ class Watermelon { public: /** * @param initialSeeds The starting number of seeds */ Watermelon(int initialSeeds); /** * Computes the seed ratio, using the Marigold algorithm. * @param slowCalc Whether or not to use long (slow) calculations 101 第Ⅰ部分 专业的 C++简介 * @return The marigold ratio */ double calcSeedRatio(bool slowCalc); }; Doxygen 可以识别 C++语法以及特定的注释指令,例如@param 以及@return,并生成定制的输 出。图 5-1 给出了 Doxygen 生成的 HTML 类引用示例。 自动生成与图 5-1 类似的文档在开发时很有用,因为这些文档允许开发人员浏览类以及类之间 关系的高层描述。您的团队可以方便地定制类似于 Doxygen 的工具来处理所采用的注释格式。理想 情况下,您的团队应该专门布置一台计算机来编写日常文档。 图 5-1 4. 特殊注释 通常您会根据需要编写注释,在此有一些在代码内使用注释的指导方针。 ● 尽量避免使用无礼的或者令人反感的语言,因为您不知道将来谁会查看您的代码。 ● 开一些内部的玩笑通常没有问题,但是应该让经理检查是否合适。 ● 在可能的情况下,给出 bug 编号或者功能 ID。 ● 如果认为某个人在将来可能会与您讨论这些注释,给出您的姓名缩写以及日期。 ● 不要为了避免承担责任而加入其他人的姓名缩写以及日期,否则您会被解雇。 ● 当更新代码的时候记得更新注释。如果代码的文档中充斥着错误信息,会让人感动非常 困惑。 ● 如果您使用注释将某个函数分为多节,考虑这个函数是否能够分解为多个更小的函数。 102 第5章 编 码 风 格 5. 自文档化代码 编写良好的代码并非总是需要充裕的注释,最优秀的代码本身就容易阅读。如果您发现在每行 都加入了注释,考虑是否可以重写这些代码以更好地配合注释中所讲的内容。记住 C++是一门语言, 其主要目的是告诉计算机做什么,但是语言的语义也可以向读者解释自身的含义。 编写自文档化(Self-documenting)代码的另一种方法是将代码分解(decompose)为小段。后面将详 细介绍分解。 良好的代码本身就容易阅读,注释只需要提供有用的附加信息。 5.2.3 本书的注释 本书中的示例代码经常使用注释来解释复杂的代码,或者指出对您可能不是那么明显的事项。 为了节省空间。我们通常会忽略前置注释以及固定格式的注释,但是我们恳切地希望您在专业的 C++ 项目中使用它们。 5.3 分解 分解(Decomposition)指将代码分为小段的行为。如果打开一个源代码文件发现一个 300 行的函 数,其中有大量嵌套的代码块,在编程的世界里没有什么比这个更让人恐惧了。理想状况下,每个 函数或者方法都应该只完成一个任务。任何子任务或者明显复杂的任务都应该分解为独立的函数或 者方法。例如,如果有人问您某个方法做什么,您回答“首先做 A,然后做 B,然后,如果满足条 件 C,那么做 D,否则做 E”。您或许应该将 A、B、C、D、E 分割为辅助方法。 分解不是精密的科学。某些程序员说函数不应该超过一页代码的长度,这是或许一个很好的经 验法则,但是您的确会发现某些 1/4 页的代码段也迫切需要分解。另一个经验法则是无论代码长短, 斜瞟代码的格式但是不阅读代码的实际内容,在任何区域代码都不应该显得太拥挤。例如,图 5-2 以及 5-3 被故意弄模糊了,因此您看不到内容。很显然,图 5-3 中的代码分解优于图 5-2。 图 5-2 图 5-3 103 第Ⅰ部分 专业的 C++简介 5.3.1 通过重构分解 当您喝了点咖啡并且确实在编程状态时,您飞快地编写代码,结果是您代码的行为确实符合预 期,但是远远谈不上优美。所有程序员都有这么做的时候,在某个项目中,这种短期的奋力编码有 时候是最具创造力的时刻。代码如果经过长时间的修改,也会变得密集。当有新的要求或者修订 bug 时,现有代码需要少量的改动。计算机术语 cruft 就是指少量代码的渐变过程,这个过程使得曾经优 雅的代码变成了补丁以及特例组成的烂摊子。 重构(refactoring)是更改代码结构的行为。下面给出了一些用来重构代码的技术,更全面的内容 请参考附录 B 列出的关于重构的书籍。 ● 增强抽象的技术: ● 封装字段:将字段设置为 protected,并给出访问字段的 getter 以及 setter 方法。 ● 让类型通用:为了更好地共享代码,创建更通用的类型。 ● 分割代码使其更为合理的技术: ● 提取方法:将较大的方法转换成易于理解的新方法。 ● 提取类:将现有类的部分代码转移到新类。 ● 增强代码名称以及位置的技巧 ● 移动方法或者移动字段:移动到更合适的类或者源文件。 ● 重命名方法或者字段:改变为更能体现出其目的的名称。 ● 上移(pull up):在 OOP 中,移动到超类。 ● 下移(push down):在 OOP 中,移动到子类。 无论代码是在开始的时候就是一堆难以理解的密集代码还是逐渐变成这样的,为了定期清理堆 积的代码,都有必要进行重构。通过重构会再次访问已有的代码并将其重写,使得代码更容易阅读 以及维护。重构是重新考虑代码分解的一个机会,如果代码的目的已经改变,或者代码在一开始就 没有被分解,当重构代码的时候,斜瞟一下代码并判断是否需要将其分解为更小的部分。 5.3.2 通过设计分解 如果您使用了模块分解,并且考虑了那一部分模块、方法以及函数可以在以后丢弃,这样的程 序通常不会像实现了全部功能的代码那样密集,并且结构更为合理。 当然,我们仍然建议在编写代码之设计您的程序。 5.3.3 本书中的分解 在本书中您会看到很多分解示例。在许多情况下,我们提到的方法不会给出实现,因为实现与 示例无关并且占用太多篇幅。 5.4 命名 计算机不关心如何命名变量以及函数,编译器以及链接器也不关心命名的方式,只要名称与其 他变量或者函数不发生冲突即可。名称可以帮助您以及您的同事理解程序的各个元素,但是程序员 在他们的程序中经常使用含糊或者不合适的名称,这种情况多得让人吃惊。 104 第5章 编 码 风 格 5.4.1 选择一个恰当的名称 变量、方法、函数或者类的名称应该能够精确描述其目的。名称还可以表达额外的信息,例如 类型或者特定的用法。当然,真正的考验是其他程序员是否理解您通过某个特定的名称所要表达的 意思。 您所在组织可能会有命名规则,除此之外并没有固定的规则。然而,有些名称基本上是不恰当 的。表 5-1 显示了一些好的名称以及不好的名称。 好名称 srcName, dstName 区别两个对象 gSettings 表明全局身份 mNameCounter 表明了数据成员身份 performCalculations() 简单,明确 mTypeString 赏心悦目 mWelshRarebit 好的内部玩笑 表 5-1 坏名称 thing1, thing2 太一般 globalUserSpecificSettingsAndPreferences 太长 mNC 太简单,太模糊 doAction() 太一般,不准确 _typeSTR256 只有计算机才会喜欢的名称 mIHateLarry 不恰当的内部玩笑 前面的表格提到了 mNC,作为成员变量这个名称太模糊太简单了。然而,对于作用域非常有限 的局部变量,允许使用短名称,例如: if (bError) { CString s; s.Format(...); AfxMessageBox(s); } CString、Format 以及 AfxMessageBox 的细节与这里讨论的主题无关。重要的是变量名称 s 非常 短,对于记忆没有帮助,但是它的作用域只有三行。这种用法是允许的,因为它不会与其他环境的 任何变量混淆。 5.4.2 命名约定 选择一个名称通常不需要太多的思考以及创造力。在许多情况下,您可以使用标准的命名技术。 下面给出的类型可以使用标准名称。 1. 计数器 在您编程生涯的早期,您可能已经看到过变量“i”用作计数器。使用 i 以及 j 分别用作计数器 105 第Ⅰ部分 专业的 C++简介 以及内部循环计数器已经成为惯例。然而要小心嵌套循环。当您想要表示“第 j 个”元素时,经常 会错误地用到“第 i 个”元素。有些程序员更喜欢使用诸如 outerLoopIndex 和 innerLoopIndex 的计 数器。 2. 前缀 许多程序员在变量名称开头用一个字符提供与变量的类型或者用法有关的信息。然而,许多程 序员并不赞成使用前缀,因为这会使得相关代码在将来变得难以维护。例如,如果某个成员变量从 静态变为非静态,这意味着所有用到这个名称的地方都要修改。这通常情况下非常耗时,因此大多 数程序员不会去重新命名这个变量。随着项目的进行,变量的声明变了,但是名称没有变。结果是 名称给出了虚假的语义,实际上这个语义是错误的。 当然,通常您别无选择,只能遵循公司的指导方针。表 5-2 显示了一些可能会用到的前缀。 前缀 m m_ _ s ms ms_ 示例名称 mData m_data _data sLookupTable msLookupTable ms_lookupTable 表 5-2 前缀的字面意思 “成员” “静态” 用法 类的数据成员 静态变量或者数据成员 k kMaximumLength “konstant”,德语表示的常量 常量值。有些程序员全部用大 写字母表示常量 b bCompleted “布尔值” 表示布尔值 n mNum nLines mNumLines “数字” 数据成员同时也是计数器。由 于 n 看上去像 m,许多程序 员用 mNum 替换 n 做前缀, 例如 mNumLines 3. getter 以及 setter 如果类包含了数据成员,例如 mStatus,习惯上会提供一个名为 getStatus()的 getter 以及一个名 为 setStatus()的 setter 访问这个成员。C++语言并未指定如何命名这些方法,但是您所在的组织可能 会采用这种命名形式或类似的形式。 4. 大写 在代码中大写名称有多种不同的方法。与大多数编码风格元素类似,最重要的是您所在的组织 将某个方法正式化,并且让所有的员工都采用这种方法。如果某些程序员全部用小写字母命名类, 106 第5章 编 码 风 格 并用下划线分隔(priority_queue),而另外一些程序员将每个单词的首字母大写(PriorityQueue),代码 将乱成一团。变量以及数据成员几乎总是以小写字母开头,并用下划线(my_queue)或者大写字母 (myQueue)分隔单词。在 C++中,函数以及方法通常将首字母大写,但是正如您看到的那样,本书 我们采用了小写风格的函数以及方法,从而把它们与类名称区分开来。采用大写字母为类以及数据 成员名指明单词的边界。 5. 把常量放到名称空间 假定您正在编写一个使用图形用户界面的程序。这个程序具有几个菜单,包括 File、Edit 以及 Help。您决定用常量代表每个菜单的 ID。kHelp 是代表 Help 菜单 ID 的一个好名字。 名称 kHelp 一直运行良好,直到有一天您在主窗口上添加了一个 Help 按钮。您需要一个常量代 表按钮的 ID,但是 kHelp 已经被使用了。 在此情况下,建议将常量放到不同的名称空间,名称空间在第 1 章已经讲过。您可以创建两个名称 空间:Menu 以及Button。每个名称空间中都有一个kHelp 常量,其用法为Menu::kHelp 以及Button::kHelp。 6. 匈牙利表示法 匈牙利表示法是关于变量以及数据成员的命名约定,Microsoft Windows 程序员经常使用这一方 法。基本思想是使用更详细的前缀而不是一个字母(例如 m)表示附加的信息。下面这行代码显示了 匈牙利表示法的用法: char* pszName; // psz means "pointer to a null-terminated string" 之所以称其为匈牙利表示法,是因为其发明者 Charles Simonyi 是一个匈牙利人。也有人认为这 准确地反映了一个事实,使用匈牙利表示法的程序员好像是在用外语编程一样。为此,一些程序员 不喜欢匈牙利表示法。在本书中我们使用前缀,但不使用匈牙利表示法。我们觉得合理命名的变量 不需要前缀以外的附加上下文信息,我们认为用 mName 命名数据成员就足够了。 好的名称会传递与用途有关的信息,而不会使得代码难以阅读。 5.5 使用具有风格的语言特性 C++语言允许您做各种非常难懂的事情。看一下这些古怪的代码: i++ + ++i; 这行代码很难懂,但是更重要的是 C++标准没有定义它的行为。问题在于 i++使用了 i 的值,同 时还有将 i 值增加的副作用。C++标准没有说明什么时候增加值,这个副作用(增量)只有在“;”之后 才能看到,但是执行到这一行时编译器可以在任意点执行增量。无法知道那一个 i 值会用于 i++部分, 在不同的编译器以及不同的平台执行这段代码会得到不同的值。下面是具有不确定行为代码的另一 个示例,您应该避免使用: a[i] = i++; 107 第Ⅰ部分 专业的 C++简介 在使用 C++语言提供的强大功能时,应该考虑如何以良好的(而不是丑陋的)风格使用语言特性, 这十分重要。 5.5.1 使用常量 不良代码经常乱用“魔法数字”。在一些函数中,代码可能使用 2.71828,为什么是 2.71828 呢? 这个值有什么含义?具有数学背景的人可会发现这明显代表 e 的近似值,但是多数人不知道这一点。 C++语言提供了常量,可以赋予某个不变的值(例如 2.718 28)一个符号名称: const double kApproximationForE = 2.71828; 5.5.2 使用引用代替指针 C++程序员通常开始学的都是 C。在 C 中,指针是唯一的按引用传递的机制,并且多年以来一 直运行良好。在某些情况下仍然需要指针,但在许多情况下可以用引用代替指针。如果您开始学习 的是 C,您可能认为引用实际上没有增加新的功能。您可能认为引用只是引入了一种新的语法,其 功能已经由指针提供。 用引用替换指针有许多好处。首先,引用比指针安全,因为不会直接处理内存地址,并且不会 是 nullptr。其次,引用在文体上比指针好,因为所用的语法与堆栈变量相同,没有使用诸如*以及& 的符号。引用易于使用,因此将引用加入到您的风格中没有任何问题。遗憾的是,某些程序员认为 如果在函数调用中看到&,他们知道被调用函数将改变对象;如果他们没有看到&,那么一定是按 值传递。使用引用时,如果没有看到函数原型,他们无法判断函数是否将改变对象的值。这种思维 方式是错误的。用指针传递未必意味着对象将会被改变,因为参数可能是 const T*。传递指针或者 引用是否会修改对象都取决于函数原型是否使用 const T*、 T*、 const T& 或者 T&。因此,为了 判断函数是否可能改变对象,无论如何您都要查看函数原型。 使用引用的另一个好处是它明确了内存的所有权。如果您编写一个方法,其他程序员传递给您 一个对象的引用,很明显您可以读取并修改这个对象,但是您无法轻易地释放对象的内存。如果传 递的是一个指针,就不那么明显。您需要删除对象来清理内存么?还是调用者需要这样做?您的团 队应该决定如何用变量传递技术表示内存所有权。处理内存较好的方法是使用智能指针,这一内容 将在第 21 章详细讨论。 5.5.3 使用自定义异常 C++可以很方便地忽略异常,这一语言的语法没有强迫您处理异常,您可以很方便地用传统的 机制(例如返回 nullptr 或者设置错误标记)编写容错程序。 异常提供了更丰富的错误处理机制,自定义异常允许您根据需要进行取舍。例如,Web 浏览器 的自定义异常类型包含的字段可以用于说明包含错误的页面、错误发生时的网络状态以及附加的环 境信息。 第 10 章将详细讲述 C++的异常。 语言特性是用来帮助程序员的,应该理解并使用有助于形成良好编程风格的特性。 108 第5章 编 码 风 格 5.6 格式 对于编码格式的争论使得许多编程团队四分五裂,友谊荡然无存。在大学里,本书作者之一参 加了一场关于在 if 语句中使用空格的争论,争论非常激烈以至于人们不得不停止讨论以确保一切 正常。 如果您所在的组织有适当的编码格式,您是幸运的。您或许不喜欢这个标准,但是至少您不需 要再讨论这个问题。如果团队中的每个人都以自己的方式编写代码,尽量容忍。正如您所看到的, 一些做法只是品味问题,但是另一些做法确实会让团队中的工作变得困难。 5.6.1 关于大括号对齐的争论 或许被议论最多的就是在那里使用区分代码块的大括号。大括号的使用有多种格式,在本书中, 除了函数、类以及方法名称之外,我们将大括号与起始语句放在同一行。下面的代码显示了这种格 式(整本书都是如此): void someFunction() { if (condition()) { cout << "condition was true" << endl; } else { cout << "condition was false" << endl; } } 这种格式节省了垂直空间,同时仍然通过缩进显示代码块。有些程序员认为节省垂直空间与实 际的编码无关。下面显示了一段冗长的代码: void someFunction() { if (condition()) { cout << "condition was true" << endl; } else { cout << "condition was false" << endl; } } 有些程序员使用水平空间时更为大方,编写的代码类似于下面的示例: void someFunction() { if (condition()) { cout << "condition was true" << endl; } else { cout << "condition was false" << endl; 109 第Ⅰ部分 专业的 C++简介 } } 当然,我们不会推荐任何特定的格式,因为我们不想收到恐吓信。 当选择说明代码块的风格时,所选的风格应该能够让读者一眼就看出某个代码块对 应的条件。 5.6.2 关于空格以及圆括号的争论 单行代码的格式也能够引起争论。我们同样不支持任何特定的方法,但将给出您可能遇到的几 种格式。 在本书中,我们在任何关键字之后都会使用空格,在运算符前后都会使用空格,在参数列表或 者函数调用中每个逗号之后都会使用空格,并使用圆括号表明操作顺序,如下所示: if (i = = 2) { j = i + (k / m); } 另一种格式将 if 当作函数,在关键字以及左括号之间没有空格。另外,在 if 语句内用于明确操 作顺序的圆括号也被省略了,因为它们没有语义相关性。 if( i = = 2 ) { j = i + k / m; } 区别十分微妙,请读者自行判断那种方法更好,然而在此我们必须指出 if 不是一个函数。 5.6.3 空格以及制表符 空格以及制表符的使用并不仅仅是格式上的偏好。如果您的团队对于空格以及制表符没有形成 约定,当程序员一起工作的时候会出大问题。当 Alice 使用 4 个空格缩进代码而 Bob 使用 5 个空格 的制表符时,最明显的问题发生了。当他们使用同一个文件时,二者都无法正确显示代码。如果 Bob 用制表符重新整理代码格式,同时 Alice 编辑同样的代码,情况更糟糕,许多源代码控制系统不能 融合 Alice 的修改。 大多数(但不是全部)编辑器可以设置空格以及制表符。某些环境甚至在读取代码的时候会调整 代码的格式,或者即使编写代码时用的是制表符,但保存的时候总是使用空格。如果环境比较灵活, 使用他人的代码会更容易。记住,制表符以及空格是不同的,因为制表符的长度不确定,而空格始 终是空格。为此,我们建议采用将制表符转换为空格的编辑器。 5.7 风格的挑战 许多程序员在项目开始的时候都保证他们将做好每件事。当变量或者参数的值永远不变时就将 其标记为 const。所有变量都具有清楚的、简明的、容易阅读的名称。每个开发人员都将左边的大括 号放在后续行,采用标准文本编辑器并遵循关于制表符以及空格的约定。 110 第5章 编 码 风 格 维持这种层次的格式一致性非常困难,原因有很多。当涉及 const 的时候,有些程序员不知道 如何用它。您总会遇到不支持 const 的旧代码或者库函数。好的程序员会使用 const_cast 暂时取消变 量的 const 属性,但是没有经验的程序员会取消来自调用函数的 const 属性,结果得到一个从不使用 const 的程序。 有时,标准化的格式会与程序员的个人口味以及偏好发生冲突。或许您的团队文化导致无法强 制使用严格的风格准则。在此类情况下,必须判断哪些元素是确实需要标准化的(例如变量名称以及 制表符),哪些元素是可以让个人决定的(或许空格以及注释格式可以这样)。您甚至可以获取或者编 写脚本从而自动纠正格式 bug,或者将格式问题与代码错误一起标记。 5.8 本章小结 C++语言提供了许多格式工具,但并没有正式说明如何使用这些工具。从根本上讲,风格约定 取决于其应用范围以及它对代码阅读性的贡献。如果您是编程团队的一员,在讨论使用什么语言以 及工具时就应该意识到这个问题。 应该承认风格是编程的一个重要方面。当把代码交给其他人之前,应该检查代码的风格。了解 好的编码风格,并采用您以及您的组织认为有用的约定。 本章是本书第 I 部分的收官之作,在较高的层次讨论了设计主题以及编码风格。第 II 部分将研 究软件工程过程的实现阶段以及 C++的编码细节。 111 1第 章 C++ I/O 揭秘 本章内容 ● 流的含义 5● 输入输出数据的时候如何使用流 ● 标准库中提供的标准流 一个程序的基本任务是接受输入和产生输出。一个不能产生任何类型输出的程序不会太有用。 所有的语言都提供了某种 I/O 机制,这种机制既有可能内建在语言中,也有可能提供操作系统特定 的 API。一个好的 I/O 系统应该兼具灵活性和易用性。灵活的 I/O 系统支持通过不同设备的输入和 输出,例如文件和用户控制台。还支持读写不同类型的数据。I/O 很容易出错,因为来自用户的数 据可能是不正确的,或者底层的文件系统或其他数据源有可能无法访问。因此,一个好的 I/O 系统 还应该能够处理错误条件。 如果您已经熟悉了 C 语言,那么您肯定使用过 printf()和 scanf()。作为 I/O 机制,printf()和 scanf() 确实很灵活。通过转义代码和变量占位符,这些函数可以定制为读取特定格式的数据,或输出格式 化代码允许的任何值,此类值局限于整数/字符值、浮点值和字符串。然而,printf()和 scanf()在优秀 I/O 系统的其他指标表现落后。这些函数不能很好地处理错误,处理自定义数据类型不够灵活,而 且最糟的是,在 C++这样的面向对象语言中,它们根本不是面向对象的! C++通过一种称为流(stream)的机制提供了更为精良的输入输出方法。流是一种灵活且面向对象 的 I/O 方法。本章介绍如何将流用于数据输出和输入。您还要学习如何通过流机制从不同的来源读 取数据,以及向不同的目标写出数据,例如用户控制台、文件甚至字符串。本章将讲解最常用的 I/O 特性。 15.1 使用流 需要花一些功夫才能习惯流的隐喻。初看上去,流似乎比传统的 C 风格 I/O(例如 printf())要复杂。 事实上,流初看上去更复杂的原因是相比于 printf(),流背后的隐喻更深刻。不过不必担心,了解了 第Ⅱ部分 专业的 C++编码方法 一些示例之后,您再也不想用旧式的 I/O 了。 15.1.1 流的含义 第 1 章将 cout 流比喻为与数据对应的洗衣滑槽。把一些变量丢到流中,这些变量就会写到用户 屏幕上,即控制台(console)上。更一般地,所有的流都可以看做是数据滑槽。流之间的区别体现在 方向以及关联的来源和目的地。例如,您已经熟悉的 cout 流是一个输出流,因此这个流的方向是“流 出”。这个流将数据写入控制台,因此这个流关联的目的地是“控制台”。还有一个称为 cin 的标准 流,这个流接受来自用户的输入。这个流的方向为“流入”,关联的来源为“控制台”。cout 和 cin 都是 C++在 std 名称空间中预定义的流实例。表 15-1 简要地描述了所有预定义的流。后面一节会解 释缓冲流和非缓冲流之间的区别。 流 cin cout cerr clog 表 15-1 说明 输入流,从“输入控制台”中读取数据 缓冲的输出流,向“输出控制台”写入数据 非缓冲的输出流,向“错误控制台”写入数据,“错误控制台”通常等同于“输出 控制台” cerr 的缓冲版本 注意,图形用户界面应用程序通常没有一个控制台,即,如果向 cout 写入一些数据,用户无法 看到。如果您在编写一个库,那么绝对不要假定存在 cout、cin、cerr 和 clog,因为不可能知道您编 写的库会应用在控制台应用程序还是 GUI 应用程序。 所有输入流都有一个关联的来源。所有输出流都有一个关联的目标。 有关流的另一个要点是流不仅包含数据,还包含一个称为当前位置(current position)的数据。当 前位置指的是流将要进行下一次读或写操作的位置。 15.1.2 流的来源和目标 流这个概念可以应用于任何接受数据或产生数据的对象。因此可以编写基于流的网络类,还可 以编写 MIDI 设备的流式访问。在 C++中,流可以使用 3 个公共的来源和目标。 您已经看到了很多用户(或控制台)流的例子。控制台输入流允许程序在运行时从用户那里获得 输入,因而使得程序具有了交互性。控制台输出流向用户提供反馈和输出结果。 顾名思义,文件流能够从文件系统中读取数据并向文件系统写入数据。文件输入流适用于读取 配置数据、读取保存的文件以及批处理基于文件的数据等任务。文件输出流适用于保存状态数据和 478 第 15 章 C++ I/O 揭秘 提供输出等任务。文件流包含了 C 语言输出函数 fprintf()、fwrite()和 fputs()的功能,还包含了 C 语 言输入函数 fscanf()、fread()和 fgets()的功能。 字符串流是将流隐喻应用于字符串类型的例子。使用字符串流的时候,可以像处理其他任何流 一样处理字符数据。就字符串流的大部分功能而言,只不过是为 string 类提供的很多方法能够完成 的功能提供了便利的语法。然而,使用流式语法为优化提供了机会,而且比直接使用 string 类方便 得多。字符串流包含了 sprintf()和 sprintf_s()的功能,以及很多 C 语言字符串格式化函数的功能。 本节主要讲解控制台流(cin 和 cout)。本章后面会列举文件流和字符串流的例子。其他类型的流, 例如打印机输出和网络 I/O 等往往和平台相关,因此本书中没有讨论这些流。 15.1.3 流式输出 第 1 章介绍了流式输出,本书中几乎每一章都使用了流式输出。本节首先简单回顾一些基本概 念,然后介绍一些更高级的内容。 1. 输出基本概念 输出流定义在头文件中。大部分程序员都会在程序中包含头文件,这个头 文件又包含输入流和输出流的头文件。头文件还声明了标准控制台输出流 cout。 使用输出流的最简单方法是使用<<运算符。通过<<可以输出 C++的基本类型,例如 int、指针、 double 和字符。此外,C++的 string 类也兼容<<,C 风格的字符串也能正确输出。下面列举一些使 用<<的示例: int i = 7; cout << i << endl; char ch = 'a'; cout << ch << endl; string myString = "Marni is adorable."; cout << myString << endl; 代码取自 OutputBasics\OutputBasics.cpp 输出如下所示: 7 a Marni is adorable. cout 流是写入到控制台的内建流,控制台也称为标准输出(standard output)。可将<<的使用串联 起来,从而输出多个数据段。这是因为<<运算符返回一个流的引用,因此可以立即对同一个流再次 应用<<运算符。例如: int j = 11; cout << "On a scale of 1 to cute, Marni ranks " << j << "!" << endl; 代码取自 OutputBasics\OutputBasics.cpp 479 第Ⅱ部分 专业的 C++编码方法 输出如下所示: On a scale of 1 to cute, Marni ranks 11! C++流可以正确地解析 C 风格的转义字符,例如包含\n 的字符串,但是完成\n 功能更好的方法 是使用内建的 endl 机制。下面的例子使用了 endl,endl 在 std 名称空间中定义,表示行结束符,并 且刷新输出缓冲区。通过一行代码可以输出多行文本。 cout << "Line 1" << endl << "Line 2" << endl << "Line 3" << endl; 代码取自 OutputBasics\OutputBasics.cpp 输出如下所示: Line 1 Line 2 Line 3 2. 输出流的方法 毫无疑问,<<运算符是输出流最有用的部分。然而,还需要了解一些额外功能。如果看一下 头文件,会发现很多重载<<运算符定义的代码行。还可以看到一些有用的公共方法。 put()和 write() put()和 write()是原始的输出方法。这两个方法接受的不是定义了输出行为的对象或变量,put() 接受一个单独的字符,write()接受一个字符数组。传给这些方法的数据按照原本的形式输出,没有 任何特殊的格式化和处理操作。例如,下面的函数接受一个 C 风格的字符串,并且将这个字符串输 出到控制台,这个函数没有使用<<运算符: void rawWrite(const char* data, int dataSize) { cout.write(data, dataSize); } 代码取自 Write\Write.cpp 下面这个函数通过 put()方法,将 C 风格字符串的给定索引输出到控制台: void rawPutChar(const char* data, int charIndex) { cout.put(data[charIndex]); } 代码取自 Put\Put.cpp flush() 向输出流写入数据的时候,流不一定会将数据立即写入目标。大部分输出流都会进行缓冲,也 就是积累数据,而不是立即将得到的数据写出去。当满足以下条件之一的时候,流进行刷新操作 (flush),即将积累的数据写出: 480 第 15 章 C++ I/O 揭秘 ● 到达某个标记的时候,例如 endl 标记。 ● 流离开作用域被析构的时候。 ● 要求从对应的输入流输入数据的时候(即要求从 cin 输入的时候,cout 会刷新)。在有关文件 流的小节中,您将学习如何建立这种连接。 ● 流缓冲满的时候。 ● 显式地要求流刷新缓冲的时候。 显式要求流刷新缓冲的方法是调用流的 flush()方法,如下代码所示: cout << "abc"; cout.flush(); cout << "def"; cout << endl; // abc is written to the console. // def is written to the console. 代码取自 Flush\fl ush.cpp 不是所有的输出流都有缓冲。例如,cerr 流就不会对输出缓冲。 3. 处理输出错误 输出错误可能会在多种情况下出现。有可能试图打开一个不存在的文件。有可能因为磁盘错误 导致写入操作失败,例如磁盘已满。到目前为止,您所读到的使用了流的代码都没有考虑这些可能 性,主要是为了代码简洁。然而,处理任何可能发生的错误是非常重要的。 当一个流处于正常的可用状态时,称这个流是“好的”。调用流的 good()方法可以判断这个流当 前是否处于好的状态。 if (cout.good()) { cout << "All good" << endl; } 通过 good()可以方便地获得流的基本验证信息,但是不能提供流不可用的原因。还有一个 bad() 方法提供了稍多信息。如果 bad()返回 true,意味着发生了致命错误(相对于非致命错误,例如遇到文 件结尾)。另一个方法 fail()在最近一次操作失败的时候返回 true,表示下一次操作也会失败。例如, 对输出流调用 flush()之后,可以调用 fail()确保流仍然可用。 cout.flush(); if (cout.fail()) { cerr << "Unable to flush to standard out" << endl; } 还可以要求流在发生故障的时候抛出异常。然后可以编写一个 catch 处理程序来捕捉 ios_base::failure 异常,然后对这个异常调用 what()方法获得错误的描述信息,调用 code()方法获得错 误代码。不过,是否能获得有用信息取决于编译器: cout.exceptions(ios::failbit | ios::badbit | ios::eofbit); try { cout << "Hello World." << endl; 481 第Ⅱ部分 专业的 C++编码方法 } catch (const ios_base::failure& ex) { cerr << "Caught exception: " << ex.what() << ", error code = " << ex.code() << endl; } 代码取自 Exceptions\Exceptions.cpp 通过 clear()方法重置流的错误状态: cout.clear(); 控制台输出流的错误检查不如文件输入输出流的错误检查频繁。这里讨论的方法也适用于其他 类型的流,后面讨论每一种类型的时候都会回顾这些方法。 4. 输出操作算子 流有一项独特的特性,那就是放入数据滑槽的内容并非仅限于数据。C++流还能识别操作算子 (manipulator),操作算子是能够修改流行为的对象,而不是(或者额外提供)流能够操作的数据。 您已经看到了一个操作算子:endl。endl 操作算子封装了数据和行为。endl 算子要求流输出一 个行结束序列,并且刷新缓冲。下面列出了其他有用的操作算子,大部分定义在 标准头文件中。列表后面的例子展示了如何使用这些操作算子: ● boolalpha 和 noboolalpha:要求流将 bool 值输出为 true 和 false(boolalpha)或 1 和 0(noboolalpha)。 默认行为是 noboolalpha。 ● hex、oct 和 dec:分别以十六进制、八进制和十进制输出数字。 ● setprecision:设置输出小数时的小数位数。这是一个参数化的操作算子(也就是说这个操作 算子接受一个参数)。 ● setw:设置输出数值数据的字段宽度。这是一个参数化的操作算子。 ● setfill:当数字宽度小于指定宽度的时候,设置用于填充的字符。这是一个参数化的操作 算子。 ● showpoint 和 noshowpoint:对于不带小数部分的浮点数,强制流总是显示或总是不显示小 数点。 ● [C++11] put_money:向流写入一个格式化的货币值。 ● [C++11] put_time:向流写入一个格式化的时间值。 下面的例子通过这些操作算子自定义输出。这个例子还使用了第 14 章讨论的 locale 概念。 // Boolean values bool myBool = true; cout << "This is the default: " << myBool << endl; cout << "This should be true: " << boolalpha << myBool << endl; cout << "This should be 1: " << noboolalpha << myBool << endl; // Simulate "%6d" with streams int i = 123; printf("This should be '123': %6d\n", i); cout << "This should be '123': " << setw(6) << i << endl; // Simulate "%06d" with streams printf("This should be '000123': %06d\n", i); 482 第 15 章 C++ I/O 揭秘 cout << "This should be '000123': " << setfill('0') << setw(6) << i << endl; // Fill with * cout << "This should be '***123': " << setfill('*') << setw(6) << i << endl; // Reset fill character cout << setfill(' '); // Floating point values double dbl = 1.452; double dbl2 = 5; cout << "This should be ' 5': " << setw(2) << noshowpoint << dbl2 << endl; cout << "This should be @@1.452: " << setw(7) << setfill('@') << dbl << endl; // Format numbers according to your location cout.imbue(locale("")); cout << "This is 1234567 formatted according to your location: " << 1234567 << endl; // C++11 put_money: cout << "This should be a money amount of 1200, " << "formatted according to your location: " << put_money("120000") << endl; // C++11 put_time: time_t tt; time(&tt); tm t; localtime_s(&t, &tt); cout << "This should be the current date and time " << "formatted according to your location: " << put_time(&t, "%c") << endl; 代码取自 Manipulator\Manipulator.cpp 如果您不关心操作算子的概念,通常也能应付过去。流通过 precision()这类方法提供了大部分 相同的功能。例如,以如下这行代码为例: cout << "This should be '1.2346': " << setprecision(5) << 1.234567 << endl; 这一行代码可以转换为方法调用: cout.precision(5); cout << "This should be '1.2346': " << 1.23456789 << endl; 代码取自 Manipulator\Manipulator.cpp 更详细信息请参阅 Wrox 网站上的标准库参考资源。 15.1.4 流式输入 输入流为结构化数据和非结构化数据的读入提供了简单方法。本节以 cin 为例讨论了输入技术, cin 即控制台输入流。 483 第Ⅱ部分 专业的 C++编码方法 1. 输入基本概念 通过输入流,可以采用两种简单方法来读取数据。第一种方法类似于<<运算符,向输出流输出 数据。读入数据对应的运算符是>>。通过>>从输入流读入数据的时候,通过代码中提供的变量保存 接受的值。例如,以下程序从用户那里读入一个单词,并将这个单词保存在一个字符串中。然后这 个字符串又被输出到控制台: string userInput; cin >> userInput; cout << "User input was " << userInput << endl; 代码取自 Input\string.cpp 默认情况下,>>运算符根据空白符对输入值标志化。例如,如果一个用户运行以上程序并且键 入 hello there 作为输入,那么只有第一个空白字符(在这个例子中为空格符)之前的字符才会被捕捉在 userInput 变量中。输出如下所示: User input was hello 在输入中包含空白字符的一种方法是使用 get(),本章后面会讨论这个方法。 >>运算符可以用于不同的变量类型,就像<<运算符一样。例如,如果要读取一个整数,那么只 有变量类型的区别: int userInput; cin >> userInput; cout << "User input was " << userInput << endl; 代码取自 Input\int.cpp 通过输入流可以读入多个值,而且可以根据需要混合和匹配类型。例如,下面这个函数摘选自 一个餐馆预订系统,这个函数要求用户输入姓以及聚会就餐的人数: void getReservationData() { string guestName; int partySize; cout << "Name and number of guests: "; cin >> guestName >> partySize; cout << "Thank you, " << guestName << "." << endl; if (partySize > 10) { cout << "An extra gratuity will apply." << endl; } } 代码取自 Input\getReservationData.cpp 注意,>>运算符会根据空白字符符号化,因此 getReservationData()函数不允许输入带有空白字 符的姓名。一个解决方法是使用本章后面讲解的 unget()方法。注意,尽管这里使用 cout 的时候没有 通过 endl 或 flush()显式地刷新缓存区,但是仍然可以将文本写入控制台,因为这里使用的 cin 立即 484 第 15 章 C++ I/O 揭秘 刷新了 cout 缓存区;cin 和 cout 通过这种方式连接在一起。 如果分不清<<和>>的作用,只要联想箭头的方向指向它们的目标即可。在输出流 中,<<指向流本身,因为数据被发送至流。在输入流中,>>指向变量,因为数据被 保存。 2. 输入方法 与输出流一样,输入流也提供了一些方法,通过这些方法可以获得比普通>>运算符更底层的 访问。 get() get()方法允许从流中读入原始输入数据。get()最简单的版本返回流中的下一个字符,还有其他 版本一次读入多个字符。get()常用于避免>>运算符的自动标志化。例如,下面这个函数从输入流中 读入一个由多个词构成的名字,直到读到流尾。 string readName(istream& inStream) { string name; while (inStream.good()) { int next = inStream.get(); if (next == EOF) break; name += next;// Implicitly convert to a char and append. } return name; } 代码取自 Get\Get.cpp 在这个 readName()函数中,有一些有趣的发现: ● 这个函数的参数是一个对 istream 的非 const 引用,而不是一个 const 引用。从一个流读入数 据的方法会改变实际的流(主要改变当前位置),因为这些方法都不是 const 方法。因此,不 能对 const 引用调用这些方法。 ● get()的返回值保存在一个 int 中,而不是一个 char 中。因为 get()会返回一些特殊的非字符的 值,例如 EOF(文件尾),因此使用 int。当 next 被追加到一个 string 的时候,被隐式地转换 为一个 char;如果被追加到一个 wstring,则会被转换为一个 wchar_t。 readName()有一点奇怪,因为可以采用两种方式跳出循环。一种方式是流进入“不好的”状态, 另一种方式是达到流尾。另一种从流中读入数据的更常用的方法是使用另一个版本的 get(),这个版 本接受一个字符的引用,并且返回一个流的引用。这种模式利用了一个事实:在条件环境中对一个 输入流求值的时候,只有当输入流可以用于下一次读取的时候才会返回 true。如果遇到错误或者到 达文件尾都会使得流求值为 false。第 18 章讲解了实现这个特性所需要的转换操作的底层细节。同 一个函数的下面这个版本稍微简洁一些: 485 第Ⅱ部分 专业的 C++编码方法 string readName(istream& inStream) { string name; char next; while (inStream.get(next)) { name += next; } return name; } 代码取自 Get\Get.cpp unget() 对于大多数场合来说,理解输入流的正确方式是将输入流理解为一个单方向的滑槽。数据丢入 滑槽,然后进入变量。unget()方法打破了这个模型,允许将数据塞回滑槽。 调用 unget()导致流回退一个位置,将前一个读入的字符放回到流中。通过调用 fail()方法可以查 看 unget()是否成功。例如,如果当前位置就是流的起始位置,那么 unget()会失败。 本章前面出现的 getReservationData()函数不允许输入一个带有空白字符的名字。下面的代码使 用了 unget(),允许名字中出现空白字符。这段代码逐字符读入,并检查字符是否为数字。如果字符 不是数字,则将字符添加到 guestName。如果字符是数字,则通过 unget()将这个字符放回到流中, 循环停止,然后通过>>运算符输入一个整数 partySize。后面的“输入操作算子”小节将讨论 noskipws 的意义。 void getReservationData() { string guestName; int partySize = 0; // Read letters until we find a non-letter char ch; cin >> noskipws; while (cin >> ch) { if (isdigit(ch)) { cin.unget(); if (cin.fail()) cout << "unget() failed" << endl; break; } guestName += ch; } // Read partysize cin >> partySize; cout << "Thank you '" << guestName << "', party of " << partySize << endl; if (partySize > 10) { cout << "An extra gratuity will apply." << endl; } } 代码取自 Unget\Unget.cpp 486 第 15 章 C++ I/O 揭秘 putback() putback()和 unget()一样,允许在输入中流反向移动一个字符。区别在于 putback()方法将从参数 接收的字符放回流中: char ch1; cin >> ch1; cin.putback(ch1); // ch1 will be the next character read off the stream. peek() 通过 peek()方法可预览调用 get()返回的下一个值。再拿滑槽的隐喻打比方,可以想象为查看一 下滑槽,但是不把值取出来。 peek()非常适合于在取出一个之前需要预先查看这个值的场合。例如下面的代码实现了 getReservationData()函数,允许名字中出现空白字符,但使用的是 peek()而不是 unget(): void getReservationData() { string guestName; int partySize = 0; // Read letters until we find a non-letter char ch; cin >> noskipws; while (true) { // 'peek' at next character ch = cin.peek(); if (!cin.good()) break; if (isdigit(ch)) { // next character will be a digit, so stop the loop break; } // next character will be a non-digit, so read it cin >> ch; guestName += ch; } // Read partysize cin >> partySize; cout << "Thank you '" << guestName << "', party of " << partySize << endl; if (partySize > 10) { cout << "An extra gratuity will apply." << endl; } } 代码取自 Peek\Peek.cpp getline() 从输入流中获得一行数据是一个非常常见的需求,所以有一个方法能完成这个任务。getline() 方法用一行数据填充字符缓冲区,数据量最多至指定大小。指定的大小中包括\0 字符。因此,下面 的代码最多从 cin 中读取 kBufferSize-1 个字符,或者读到行尾为止: 487 第Ⅱ部分 专业的 C++编码方法 char buffer[kBufferSize]; cin.getline(buffer, kBufferSize); 代码取自 Getline\Getline.cpp 调用 getline()的时候,这个方法从输入流中读取一行,读到行尾为止。不过,行尾字符不会出 现在字符串中。注意,行尾序列和平台相关。例如,行尾序列可以是\r\n、\n 或\n\r。 有一个版本的 get()执行的操作和 getline()一样,区别在于 get()把换行序列留在输入流中。 还有一个用于 C++ string 的名为 getline()的函数。这个函数定义在头文件和 std 名称空间 中。这个函数接受一个流引用,一个 string 引用以及一个可选的分隔符作为参数: string myString; std::getline(cin, myString); 代码取自 Getline\Getline.cpp 3. 处理输入错误 输入流提供了一些方法用于检测异常情形。大部分和输入流有关的错误条件都发生在无数据可 读的时候。例如,可能到达了流尾(称为文件尾,即使不是文件流)。查询输入流状态的最常见方法 是在条件语句中访问输入流。例如,只要 cin 保持在“好的”状态,下面的循环持续进行: while (cin) { ... } 同时可以输入数据: while (cin >> ch) { ... } 还可以调用 good()方法,就像输出流那样。还有一个名为 eof()的方法,如果流到达尾部的时候 返回 true。 您还应该养成一个读取数据后就检查流状态的习惯,这样可以从异常输入中恢复。 下面的程序展示了从流中读取数据并处理错误的常用模式。这个程序从标准输入中读取数字, 到达文件结尾的时候显示这些数字的总和。注意在命令行环境中,需要用户键入一个特殊的字符表 示文件结束。在 Unix 和 Linux 中,这个特殊的字符是 Control+D,在 Windows 中为 Control+Z。具 体的字符与操作系统相关,因此您还需要了解操作系统要求的字符: cout << "Enter numbers on separate lines to add. " << "Use Control+D to finish (Control+Z in Windows)." << endl; int sum = 0; if (!cin.good()) { cerr << "Standard input is in a bad state!" << endl; return 1; } int number; while (true) { cin >> number; if (cin.good()) { sum += number; } else if (cin.eof()) { 488 第 15 章 C++ I/O 揭秘 break; // Reached end of file } else { // Error! cin.clear(); // Clear the error state. string badToken; cin >> badToken; // Consume the bad input. cerr << "WARNING: Bad input encountered: " << badToken << endl; } } cout << "The sum is " << sum << endl; 代码取自 ErrorCheck\ErrorCheck.cpp 4. 输入操作算子 下面的列表中列出了内建的输入操作算子,输入操作算子可以发送到输入流中以便自定义数据 读入的行为。 ● boolalpha 和 noboolalpha:如果使用了 boolalpha,字符串 false 会解释为布尔值 false;其他 任何字符串都会被解释为布尔值 true。如果设置了 noboolalpha,0 会被解释为 false,其他任 何值都被解释为 true。默认行为是 noboolalpha。 ● hex、oct 和 dec:分别以十六进制、八进制和十进制读入数字。 ● skipws 和 noskipws:告诉输入流在标记化的时候跳过空白字符,或者读入空白字符作为 标记。 ● ws:一个简便的操作算子,表示跳过流中当前位置的一串空白字符。 ● [C++11] get_money:从流中读入一个货币值。 ● [C++11] get_time:从流中读入一个格式化的时间值。 输入支持 locale。例如,下面的代码将 cin 的 locale 设置为系统 locale。第 14 章讨论了 locale: cin.imbue(locale("")); int i; cin >> i; 如果系统 locale 为 U.S. English,那么输入 1,000 会被解析为 1000。如果系统 locale 为 Dutch Belgium,那么输入 1.000 会被解析为 1000。 15.1.5 对象的输入输出 如果不是基本类型,也可以通过<<运算符输出一个 C++字符串。在 C++中,对象可以描述其输 出和输入的方式。这是通过重载<<和>>运算符完成的,重载的运算符可以理解新的类型或类。 为什么要重载这些运算符?如果您已经熟悉了 C 语言中的 printf()函数,那么您应该知道 printf() 在这方面并不灵活。尽管 printf()知道多种数据类型,但是无法让其知道更多的知识。例如,考虑下 面这个简单的类: class Muffin { public: string getDescription() const; 489 第Ⅱ部分 专业的 C++编码方法 void setDescription(const string& inDesc); int getSize() const; void setSize(int inSize); bool getHasChocolateChips() const; void setHasChocolateChips(bool inChips); protected: string mDesc; int mSize; bool mHasChocolateChips; }; string Muffin::getDescription() const { return mDesc; } void Muffin::setDescription(const string& inDesc) { mDesc = inDesc; } int Muffin::getSize() const { return mSize; } void Muffin::setSize(int inSize) { mSize = inSize; } bool Muffin::getHasChocolateChips() const { return mHasChocolateChips; } void Muffin::setHasChocolateChips(bool inChips) { mHasChocolateChips = inChips; } 代码取自 Muffin\Muffi n.cpp 为了通过 printf()输出 Muffin 类的对象,如果能将其指定为参数,然后再用%m 这样的占位符就 好了。 printf("Muffin output: %m\n", myMuffin); // BUG! printf doesn't understand Muffin. 遗憾的是,printf()函数完全不了解 Muffin 类型,因此无法输出 Muffin 类型的对象。最糟糕的情 况是,由于 printf()函数的声明方式,这样的代码会导致运行时错误,而不是一个编译时错误(不过一 个好的编译器会给出一个警告消息)。 如果要使用 printf(),最多在 Muffin 类中添加一个新的 output()方法。 class Muffin { public: string getDescription() const; void setDescription(const string& inDesc); int getSize() const; void setSize(int inSize); bool getHasChocolateChips() const; void setHasChocolateChips(bool inChips); void output(); protected: string mDesc; int mSize; bool mHasChocolateChips; }; // Other method implementations omitted for brevity void Muffin::output() { printf("%s, Size is %d, %s\n", getDescription().c_str(), getSize(), (getHasChocolateChips() ? "has chips" : "no chips")); } 代码取自 Muffin\Muffin.cpp 490 第 15 章 C++ I/O 揭秘 不过,使用这种机制非常笨拙。如果要在另一行文本的中间输出一个 Muffin,那么需要将这一 行分解为两个调用,在两个调用之间插入一个 Muffin::output()调用,如下所示: printf("The muffin is "); myMuffin.output(); printf(" -- yummy!\n"); 通过重载<<运算符,Muffin 的输出就像输出一个 string 一样简单——只要将其作为<<的参数即 可。第 18 章讲解了运算符<<和>>的重载。 15.2 字符串流 可以通过字符串流将流语义用于 string。通过这种方式,可得到一个内存内的流(in memory stream),通过这个流表示文本数据。例如,在一个 GUI 应用程序中,可能需要用流来构建文本数据, 但是不想将文本输出到控制台或文件中,而是想要作为结果显示在 GUI 元素中,例如消息框和编辑 框。另一个例子是,假如想要将一个字符串流作为参数传给不同函数,同时想要维护当前的读位置, 这样每一个函数都可以处理流的下一部分。字符串流也非常适于解析文本,因为流内建了标记化的 功能。 ostringstream 类用于将数据写入 string,istringstream 用于将数据从一个 string 中读出。这两个类 都定义在头文件中。由于 ostringstream 和 istringstream 分别继承了来自 ostream 和 istream 的同样功能,因此这些类的使用也非常类似。 下面的程序从用户那里请求单词,然后将这些单词输入到一个 ostringstream 中,通过制表符将 单词分开。在程序的最后,整个流通过 str()方法转换为一个 string 对象,并写入控制台。可以通过 输入标记“done”来停止标记的输入,或通过按下 Control+D(Unix)或 Control+Z(Windows)来关闭输 入流。 cout << "Enter tokens. Control+D (Unix) or Control+Z (Windows) to end" << endl; ostringstream outStream; while (cin) { string nextToken; cout << "Next token: "; cin >> nextToken; if (nextToken == "done") break; outStream << nextToken << "\t"; } cout << "The end result is: " << outStream.str(); 代码取自 StringStream\StringStream.cpp 从一个字符串流中读入数据非常类似。下面的函数创建一个 Muffin 对象,并通过从字符串输入 流中读入的数据填充这个对象(参见此前的例子)。流数据格式固定,因此这个函数可以轻松地将数 据值转换为对 Muffin 类的设置方法的调用: 491 第Ⅱ部分 专业的 C++编码方法 Muffin createMuffin(istringstream& inStream) { Muffin muffin; // Assume data is properly formatted: // Description size chips string description; int size; bool hasChips; // Read all three values. Note that chips is represented // by the strings "true" and "false" inStream >> description >> size >> boolalpha >> hasChips; muffin.setSize(size); muffin.setDescription(description); muffin.setHasChocolateChips(hasChips); return muffin; } 代码取自 Muffin\Muffin.cpp 将一个对象转换为一个“扁平”类型(例如 string)的过程通常称为编组(marshall)。 将对象保存至磁盘或通过网络发送的时候,编组操作非常有用。 相比于标准 C++ string,字符串流最主要的好处是除了数据之外,这个对象还知道从哪里进行 下一次读或写操作,这个位置也称为当前位置。根据特定的字符串流的实现,可能还会有性能优势。 例如,如果需要将大量字符串串联在一起,使用字符串流的效率可能比反复调用 string 对象的+=运 算符的效率更高。 15.3 文件流 文件本身非常符合流的抽象,因为文件读写的时候除了数据之外还涉及读写的位置。在 C++中, ofstream 和 ifstream 类提供了文件的输出和输入功能。这两个类在头文件中定义。 在处理文件系统的时候,错误情形的检测和处理非常重要。您正在处理的文件可能在一个刚刚 下线的网络存储中,或者您可能写入一个已满磁盘上的文件。也许您试图打开一个用户没有访问权 限的文件。可以通过前面描述的标准错误处理机制检测到错误情形。 输出文件流和其他输出流的一个主要区别在于:文件流的构造函数可以接受文件名以及打开文 件的模式作为参数。默认模式是写文件,ios_base::out,这个模式从文件开头写文件,改写任何已有 的数据。在文件流构造函数的第二个参数指定常量 ios_base::app,还可按追加模式打开输出文件流。 表 15-2 列出了可供使用的不同常量: 492 第 15 章 C++ I/O 揭秘 常量 ios_base::app ios_base::ate ios_base::binary ios_base::in ios_base::out ios_base::trunc 表 15-2 说明 打开文件,在每一次写操作之前,移到文件末尾 打开文件,打开之后立即移到文件末尾 以二进制模式执行输入和输出操作(相对于文本模式) 打开文件作为输入 打开文件作为输出 打开文件,并截断任何已有数据 下面的程序打开文件 test,并输出程序的参数。ifstream 和 ofstream 的析构函数会自动关闭底层 文件,因此不需要显式调用 close(): int main(int argc, char* argv[]) { ofstream outFile("test"); if (!outFile.good()) { cerr << "Error while opening output file!" << endl; return -1; } outFile << "There were " << argc << " arguments to this program." << endl; outFile << "They are: " << endl; for (int i = 0; i < argc; i++) { outFile << argv[i] << endl; } return 0; } 代码取自 FileStream\FileStream1.cpp 15.3.1 通过 seek()和 tell()在文件中转移 所有的输入和输出流都有 seek()和 tell()方法,但是在文件流的上下文之外很少有意义。 seek()方法允许在输入或输出流中移动到任意位置。seek()有好几种形式。输入流中的 seek()方法 实际上称为 seekg()(g 表示 get 的意思),输出流中 seek()的版本称为 seekp()(p 表示 put 的意思)。您可 能想知道为什么同时存在 seekg()和 seekp()方法,而不是一个 seek()方法。原因是有的流既可以输入 又可以输出,例如文件流。在这种情况中,流需要记住一个读位置和一个独立的写位置。另外还有 本章后面讨论的双向 I/O。 seekg()和 seekp()有两个重载。一个重载接受一个参数:绝对位置,这个重载定位到这个绝对位 置。另一个重载接受一个偏移量和一个位置,这个重载定位到给定位置相对偏移量的位置。位置的 493 第Ⅱ部分 专业的 C++编码方法 类型为 ios_base::streampos,偏移量的类型为 ios_base::streamoff,这两个类型都以字节计数。预定义 的三个位置如表 15-3 所示。 位置 ios_base::beg ios_base::end ios_base::cur 表 15-3 表示流的开头 表示流的结尾 表示流的当前位置 说明 例如,要定位到输出流的一个绝对位置,可以使用接受一个参数的 seekp()版本,如下例所示, 这个例子通过 ios_base::beg 常量定位到流的开头位置: outStream.seekp(ios_base::beg); 在输入流中定位完全一样,只不过用的是 seekg()方法: inStream.seekg(ios_base::beg); 接受两个参数的版本定位到流中的相对位置。第一个参数表示要移动的位置数,第二个参数表 示起始点。要相对文件起始位置移动,使用 ios_base::beg 常量。要相对文件末尾位置移动,使用 ios_base::end 常量。要相对文件当前位置移动,使用 ios_base::cur 常量。例如,下面这行代码从流起 始位置移动到第二个字节。注意,整数被隐式地转换为 ios_base::streampos 和 ios_base::streamoff 类型: outStream.seekp(2, ios_base::beg); 下面这个例子转移到输入流中倒数第 3 个字节: inStream.seekg(-3, ios_base::end); 可以通过 tell()方法查询流的当前位置,这个方法返回一个表示当前位置的 ios_base::streampos 值。利用这个结果,可在进行 seek()之前记住当前标记的位置,还可以查询是否在某个特定的位置。 和 seek()一样,输入流和输出流也有不同版本的 tell()。输入流使用的是 tellg(),输出流使用的是 tellp()。 下面的代码检查输入流的当前位置,并判断是否在起始位置: ios_base::streampos curPos = inStream.tellg(); if (ios_base::beg == curPos) { cout << "We're at the beginning." << endl; } 下面是一个整合了所有内容的示例程序。这个程序写入一个名为 test.out 的文件,并且执行以下 测试: (1) 将字符串 12345 输出至文件。 494 第 15 章 C++ I/O 揭秘 (2) 验证标记在流中的位置 5。 (3) 转移到输出流的位置 2。 (4) 在位置 2 输出 0,并关闭输出流。 (5) 在文件 test.out 文件上打开输入流。 (6) 将第一个标记以整数的形式读入。 (7) 确认这个值是否为 12045。 ofstream fout("test.out"); if (!fout) { cerr << "Error opening test.out for writing" << endl; return 1; } // 1. Output the string "12345". fout << "12345"; // 2. Verify that the marker is at position 5. ios_base::streampos curPos = fout.tellp(); if (5 == curPos) { cout << "Test passed: Currently at position 5" << endl; } else { cout << "Test failed: Not at position 5" << endl; } // 3. Move to position 2 in the stream. fout.seekp(2, ios_base::beg); // 4. Output a 0 in position 2 and close the stream. fout << 0; fout.close(); // 5. Open an input stream on test.out. ifstream fin("test.out"); if (!fin) { cerr << "Error opening test.out for reading" << endl; return 1; } // 6. Read the first token as an integer. int testVal; fin >> testVal; // 7. Confirm that the value is 12045. const int expected = 12045; if (testVal == expected) { cout << "Test passed: Value is " << expected << endl; } else { cout << "Test failed: Value is not " << expected << " (it was " << testVal << ")" << endl; } 代码取自 FileStream\FileStream2.cpp 15.3.2 将流连接在一起 任何输入和输出流之间都可以建立连接,从而实现“访问时刷新”的行为。换句话说,当从输 入流请求数据的时候,连接的输出流会自动刷新。这种行为可用于所有流,但是对于互相可能存在 依赖关系的文件流来说特别有用。 495 第Ⅱ部分 专业的 C++编码方法 通过 tie()方法完成流的连接。要将输出流连接至一个输入流,对输入流调用 tie()方法,并且传 入输出流的地址。要解除连接,传入 nullptr。 下面的程序将一个文件的输入流连接至一个完全不同的文件的输出流。您可以连接至同一个文 件的输出流,但是双向 I/O(详见稍后的描述)可能是实现同时读写同一个文件的更优雅的方式。 ifstream inFile("input.txt"); ofstream outFile("output.txt"); // Set up a link between inFile and outFile. inFile.tie(&outFile); // Output some text to outFile. Normally, this would // not flush because std::endl was not sent. outFile << "Hello there!"; // outFile has NOT been flushed. // Read some text from inFile. This will trigger flush() // on outFile. string nextToken; inFile >> nextToken; // outFile HAS been flushed. 代码取自 tie\tie.cpp 在 ostream 基类上定义 flush()方法,因此可将一个输出流连接至另一个输出流: outFile.tie(&anotherOutputFile); 这种关系意味着:每次写入一个文件的时候,发送给另一个文件的缓冲数据会被写入。可以通 过这种机制保持两个相关文件的同步。 这种流连接的一个例子是 cout 和 cin 之间的连接。每当从 cin 输入数据的时候,cout 都会自动 刷新。 15.4 双向 I/O 目前,本章已经讨论了输入和输出流,讨论的时候把这两个类当做独立但又关联的类。事实上, 有一种流可以同时执行输入和输出。双向流可以同时以输入流和输出流的方式操作。 双向流是 iostream 的子类,而 iostream 是 istream 和 ostream 的子类,因此这是一个多重继承的 实例。跟直觉一样,双向流同时支持>>运算符和<<运算符,还支持输入流和输出流的方法。 fstream 类提供了双向文件流。fstream 特别适合于需要替换文件中数据的应用程序,因为可以通 过读取文件找到正确的位置,然后立即切换为写入文件。例如,假设一个程序保存一个 ID 号和电 话号码之间的映射列表。这个程序可能使用以下格式的数据文件: 123 408-555-0394 124 415-555-3422 164 585-555-3490 100 650-555-3434 一个合理的方案是当这个程序打开文件的时候读取整个数据文件,然后在程序结束的时候,将 所有的变化重新写入这个文件。然而,如果数据集巨大,可能无法把所有数据都保存在内存中。如 496 第 15 章 C++ I/O 揭秘 果使用 iostream,则不需要这样。您可以轻松扫描文件找到记录,然后以追加模式打开输出文件来 添加新记录。如果要修改已有记录,可以使用双向流,例如下面的函数替换指定 ID 的电话号码: bool changeNumberForID(const string& inFileName, int inID, const string& inNewNumber) { fstream ioData(inFileName.c_str()); if (!ioData) { cerr << "Error while opening file " << inFileName << endl; return false; } // Loop until the end of file while (ioData.good()) { int id; string number; // Read the next ID. ioData >> id; // Check to see if the current record is the one being changed. if (id == inID) { // Seek to the current read position ioData.seekp(ioData.tellg()); // Output a space, then the new number. ioData << " " << inNewNumber; break; } // Read the current number to advance the stream. ioData >> number; } return true; } 代码取自 Bidirectional\Bidirectional.cpp 当然,只有在数据大小固定的时候这种方法才能正常工作。当以上程序从读取切换到写入的时 候,输出数据会改写文件中的其他数据。为了保持文件的格式,并且避免写入下一条记录,数据必 须相同大小。 还可以通过 stringstream 类双向访问字符串流。 双向流用不同的指针保存读位置和写位置。在读取和写入之间切换的时候,需要 定位到正确的位置。 15.5 本章小结 流为输入和输出提供了一种灵活且面向对象的方式。本章中最重要的内容是流的概念,这个概 念甚至比流的使用还要重要。有一些操作系统可能有自己的文件访问和 I/O 工具,但是掌握了流和 以流的方式工作的库的知识是使用任何类型现代 I/O 系统的关键。 497 2第 章 开发跨平台和跨语言的应用程序 本章内容 ● 如何编写在多个平台上运行的代码 5● 如何混合使用不同的编程语言 C++程序可以编译为运行在不同的计算平台上,这个语言的定义严格,确保了在一个平台上用 C++编程非常类似于在另一个平台上用 C++编程。然而,尽管语言是标准化的,但是在编写专业水 准的 C++程序时仍然需要考虑平台的差异。即使开发限定于特定平台,编译器之间微小的差别也会 引发编程中令人感到头疼的问题。本章将深入研究多平台和多编程语言编程的复杂性。 本章第一部分考察了 C++程序员会遇到的与平台相关的问题。一个平台是构成开发环境和运行 时系统的所有细节的集合。例如,您的平台可能是运行在 Windows 7 上的 Microsoft Visual C++ 2010 编译器,其中处理器为 Intel Core i7 处理器。另外,您的平台也可能是运行在 Linux 上的 GCC 4.6 编译器,其中处理器为 PowerPC。这些平台都能够编译和运行 C++程序,但它们之间存在显著差异。 本章的第二部分探讨 C++可以如何与其他编程语言交互。尽管 C++是一个通用的编程语言,但 是并不一定适合于所有任务。通过各种机制,可将 C++和其他语言集成在一起,以便更好地满足自 己的需要。 25.1 跨平台开发 因为一些原因,C++语言会遇到平台的问题。尽管 C++是一个高层次的语言,但是 C++的定义 包括了低层次的实现细节。例如,C++数组定义为存在于连续的内存块中。由于并不是所有系统都 采用同样的方式安排和管理内存,所以这样一个特定的实现细节使得这个语言暴露于不利的情况。 C++还面临一个挑战:提供了一个标准的语言和一个标准的库,但是没有一个标准的实现。不同 C++ 编译器和库的提供商对规范理解的不同使得一个系统到另一个系统的迁移变得困难。最后,C++对 于语言提供什么作为标准具有选择性。尽管存在一个标准库,但是复杂程序往往需要采用语言或标 准库没有提供的功能。这些功能一般来自第三方库或平台,而且可能差别巨大。 第Ⅳ部分 C++软件工程 25.1.1 硬件架构问题 硬件架构这个词通常指的是运行程序的处理器或一族处理器。运行 Windows 或 Linux 的标准 PC 通常运行在 x86 架构上,较老版本的 Mac OS 通常运行在 PowerPC 架构上。作为一门高级语言,C++ 屏蔽了这些硬件架构之间的差异。例如,Pentium 处理器可能有一条指令需要用 6 条 PowerPC 指令 才能实现同样的功能。作为一名 C++程序员,您不需要理解这个差异,甚至不需要知道存在这种差 异。使用高级语言的一个优势是:编译器会负责将您的代码转换为处理器原生的汇编代码格式。 然而,处理器的区别有时候会上升到 C++代码的层次。除非您在完成最底层的编码工作,否则 您基本上不会面对这些问题,但是您应该意识到这些问题的存在。 1. 二进制兼容性 您可能已经知道,不能将为 Pentium 计算机编写和编译的代码放在基于 PowerPC 的 Mac 计算机 上运行。这两个平台没有二进制兼容,因为这两个平台的处理器不支持同一个指令集。当您编译一 个 C++程序的时候,您的源代码被转换为计算机执行的二进制指令。这个二进制指令的格式是由平 台定义的,而非由 C++定义。 支持二进制不兼容平台的一种方法是利用不同目标平台的编译器分别生成每一个版本。 另一个解决方案是交叉编译。例如您使用平台 X 作为开发平台,但是您需要让您的程序运行在 平台 Y 和 Z 上,您可以在平台 X 上使用交叉编译器生成平台 Y 和 Z 的二进制代码。 您也可以开放您的程序的源代码。通过向最终用户提供源代码,用户可以在自己的系统上原生 地编译代码,并构建一个适合计算机的正确二进制格式的版本。根据第 2 章中所讨论的,开源软件 已经越来越受欢迎。主要原因之一就是开源软件允许程序员合作开发软件,增加软件可以运行的平 台数。 2. 地址大小 当有人描述一个硬件架构是 32 位的时候,他们最有可能说的是地址大小为 32 位,也就是 4 字 节。在一般情况下,具有更大地址空间的系统可以处理更多内存,并且可以更快地处理复杂程序。 由于指针是内存地址,所以指针的大小和地址大小本质上是绑定的。许多程序员被教导指针总 是 4 个字节,但这是错误的。例如,考虑下面这个输出指针大小的程序: int *ptr; cout <<"ptr size is " << sizeof(ptr) << " bytes" << endl; 代码取自 PtrSize\PtrSize.cpp 如果这个程序在 32 位 x86 系统上编译运行,例如 Pentium 架构,输出将是: ptr size is 4 bytes 如果通过 64 位的编译器在 64 位 x86 系统上编译运行,如 Intel Core i7,输出将是: ptr size is 8 bytes 从程序员的角度来看,不同的指针大小意味着不能把指针等同地看做 4 字节。更普遍地讲,您 必须要知道,大多数大小都不是 C++标准规定的。标准只说了短整数应该不大于整数,整数不大于 782 第 25 章 开发跨平台和跨语言的应用程序 长整数。 指针的大小也不一定和整数的大小一样大。例如,一个 64 位的平台上,指针是 64 位的,但整 数可能是 32 位的。将一个 64 位的指针强制转换为一个 32 位的整数将丢失 32 个关键位! 提示:永远不要假设一个指针是 32 位或 4 个字节,永远不要将一个指针强制类 型转换为一个整数。 3. 字节顺序 所有现代计算机都存储二进制表示的数字,但是同一个数在两个平台上的表示可能不同。这听 起来很矛盾,但您会看到,有两种读数的方法使得两个数字都有意义。 计算机内存中的一个槽通常都是一个字节,因为大部分计算机都是按字节寻址的。C++中的数 字类型通常都是多字节的。例如,short 可能是 2 个字节。假设程序中有下面这一行: short myShort = 513; 在二进制中,数字 513 表示为 0000 0010 0000 0001。这个数字包含 16 个 0 或 1,即 16 位。因 为一个字节有 8 位,计算机需要 2 个字节来存储这个数字。因为每个内存地址都包含 1 个字节,计 算机需要将这个数字分成多个字节。假设 short 是 2 个字节,这个数字将平均分解为两个部分。这个 数字的上部分放在高字节中,下半部分放在低字节中。这种情况下,高字节为 0000 0010,低字节为 0000 0001。 现在数字已经被分解为内存大小的部分了,唯一剩下的问题如何把它们保存在内存中。两个字 节是必要的,但字节的顺序是不清楚,事实上顺序取决于当前采用的系统硬件架构。 表示数字的一种方法是首先将高字节放在内存中,然后将低字节放在内存中。这种策略称为 big-endian(大尾)顺序,因为数字中更大的一部分放在前面。PowerPC 和 Sparc 处理器使用大尾顺序。 其他一些处理器,如 x86,将字节以相反的顺序保存,先把低字节放在内存中。这种方法称为 little-endian(小尾)顺序,数字中更小的一部分放在前面。一个架构可以选择一种方法或另一种方法, 通常是根据向后兼容性选择。“大尾”和“小尾”这两个术语早于现代计算机数百年就出现了。Joathan Swift 在他 18 世纪的小说《格列夫游记》用这两个词描述了两个阵营对于用哪一头敲鸡蛋的争论。 不论某个架构使用的是什么顺序,您的程序使用数值的时候都不必关心底层机器使用的是大尾 顺序还是小尾顺序。这个顺序只有在不同架构之间转移数据的时候才需要考虑。例如,如果通过网 络发送二进制数据,则可能需要考虑其他系统使用的顺序。一个解决办法是使用标准的网络字节顺 序(Network Byte Ordering),这个顺序始终是大尾顺序。因此,通过网络发送数据之前,将数据转换 为大尾顺序,从网络接收的数据的时候,将数据从大尾顺序转换为底层系统使用的顺序。 同样,如果将二进制数据写入文件,您可能要考虑打开文件的系统使用不同字节顺序的情况。 25.1.2 实现问题 编写 C++编译器时,编译器的设计者试图遵循 C++标准。遗憾的是,C++标准有一千多页长, 混合了文字叙述、语言语法和示例。两个根据这样的标准实现一个编译器的人都不可能以完全相同 的方式解释标准中每一条信息,也不可能处理每一个边缘情况。因此,编译器会有 bug。 783 第Ⅳ部分 C++软件工程 1. 编译器的怪异特性和扩展 没有简单的规则能发现或避免编译器 bug。您能做的最好的就是保持编译器最新,或订阅编译 器的邮件列表或新闻组。如果您怀疑遇到了一个编译器 bug,可以在网上简单地搜索您看到的错误 消息或条件,有可能能找到解决方法或补丁。 编译器产生麻烦最臭名昭著的一个方面是语言最初始标准中没有的扩充。例如,C++中有一些 模板和运行时类型的特性最初不是标准的一部分,因而一些编译器仍然没有加入这些特性。使用 C++11 中的新特性的时候您也会遇到同样的问题。并非所有编译器都支持每个新功能。 另一个要注意的问题是,编译器通常包括自己对语言的扩展,而没有让程序员清楚地了解这些 扩展。例如,基于堆栈的大小可变数组不是 C++语言的一部分,但下面的代码在使用 g++编译器编 译的时候能按照期望的那样编译和运行: int i = 4; char myStackArray[i]; // Not a standard language feature! 代码取自 VariableArray\VariableArray.cpp 一些编译器扩展可能是有用的,但是如果某个时候您需要切换编译器,那么您应该检查编译器 是否处于禁用了这些扩展的严格模式。例如,编译以上代码的时候向 g++传入-pedantic 标志会得到 以下警告消息: warning: ISO C++ forbids variable length array 'myStackArray' [-Wvla] C++规范允许通过#pragma 机制使用特定类型的编译器定义的语言扩展。#pragma 是一个预编译 指令,这条指令的行为时由编译器的实现定义的。如果某个实现不理解这条指令,则会忽略这条指 令。例如,有些编译器允许程序员通过#pragma 暂时关闭编译器的警告。 2. 库的实现 您的编译器多半实现了一个 C++标准库,包括标准模板库。然而,由于 STL 是用 C++编写的, 因此并不要求您使用编译器自带的那个 STL。例如,您可以使用一个针对速度优化的第三方 STL, 甚至可以编写自己的 STL。 当然,STL 实现者与编译器编写者面临着同样的问题——标准的实现受到对标准解释的影响。 此外,某些实现可能作出权衡,而这种权衡可能不符合您的需求。例如,一个实现可能针对速度优 化,而另一个实现则可能关注于让容器尽可能使用少的内存。 使用 STL 实现或任何第三方库的时候,重要的是要考虑设计者在开发过程中所做的权衡。第 2 章更详细地讨论了使用库的时候遇到的问题。 25.1.3 平台相关的特性 C++是一门卓越的通用语言。加上标准库,这个语言带有了太多的特性,多年来业余程序员可 以轻松编写 C++代码,而不用考虑内建特性之外的特性。然而,专业的程序需要使用 C++没有提供 的设施。本节列出了一些由平台提供的重要特性,这些特性不是 C++语言提供的。 ● 图形用户界面:如今大多数商业程序度运行在一个有图形用户界面的操作系统上,图形用 户界面的元素包括可点击的按钮、可移动的窗口以及分层菜单等。C++和 C 语言一样,都 784 第 25 章 开发跨平台和跨语言的应用程序 没有这些元素的概念。要用 C++编写一个图形应用程序,需要使用特定于平台的库才能绘 制窗口、通过鼠标接受输入并执行其他图形任务。 ● 网络:因特网已经改变了我们编写应用程序的方式。如今,大多数应用程序都通过网络检 查更新,游戏都提供了联网多人模式。尽管有好几个标准库,但是 C++没有提供网络机制。 编写网络软件的最常用方法是使用一种名为套接字(socket)的抽象。大部分平台上都能看到 套接字库的实现,套接字为网络数据传输提供了一种简单的面向过程的方法。某些平台支 持基于流的网络系统,操作类似 C++中的 I/O 流。由于 IPv4 的 IP 地址紧缺,IPv4 的后继者 IPv6 很快就要取代 IPv4 了。因此,选择一个 IP 版本无关的网络库比选择只支持 IPv4 的库 更好。 ● 操作系统事件和应用程序交互:纯 C++代码和周围的操作系统和其他应用程序的交互很少。 在标准的没有使用平台扩展的 C++程序中,命令行参数是程序获得的所有内容。例如,复 制和粘贴这类操作在 C++中就不能直接支持,而是需要平台提供的库。 ● 低级文件操作:第 15 章讲解了 C++中的标准 I/O 操作,包括读取和写入文件。很多操作系 统都提供了自己的文件 API,有时这些 API 和 C++中标准的文件类不兼容。这些库通常提 供了特定于操作系统的文件工具,如获得当前用户主目录的机制。 ● 线程:C++03 和之前版本的 C++没有直接支持在一个程序中并发线程的执行。C++11 包含 了一个线程库,详见第 22 章。如果您的编译器还不支持 C++11 的线程库,您需要使用第三 方库。最常用的第三方线程库是 pthreads。很多操作系统和面向对象的框架还提供了自己的 线程模型。 25.2 跨语言开发 对于某些类型的程序,C++可能并不是完成这项工作的最佳工具。例如,如果您的 Unix 程序需 要和 shell 环境密切交互,那么您最好写一个 shell 脚本而不是一个 C++程序。如果您的程序执行繁 重的文字处理,您可能会认为 Perl 语言是最好的选择。有时候您需要的语言融合了 C++的通用特性 和其他语言的特殊特性。幸运的是,通过一些技术您可以使用两个世界中最好的部分—— C++的灵 活性和其他语言的独特特性的结合。 25.2.1 混合使用 C 和 C++ 您已经知道,C++语言是 C 语言的超集。除了几个很小的例外,所有 C 程序都可以在 C++中编 译和运行。这些例外通常都与保留字有关。例如,在 C 语言中,class 这个词没有特别的意义。因此 可以作为一个变量名,例如下面的 C 代码: int class = 1; // Compiles in C, not C++ printf("class is %d\n", class); 代码取自 MixingC\MixingC.cpp 这个程序可以在 C 语言中编译运行,但作为 C++代码编译的时候会得到一个错误。当您将一个 C 语言程序翻译(或移植)为 C++,有可能会遇到几种类型的错误。幸运的是,这些问题的修复通常 785 第Ⅳ部分 C++软件工程 很简单。在这个例子中,将 class 变量重命名为 ClassID,代码将成功编译。 当您遇到一个用 C 语言编写的有用的库或遗留代码时,C 语言代码和 C++程序结合的方便性就 很有用。本书中您已经多次看到,函数和类可以很好地在一起工作。一个类方法可以调用一个函数, 一个函数可以使用对象。 25.2.2 转移范例 C 和 C++混合编程的危险之一就是您的程序可能会开始失去其面向对象的特性。例如,如果一 个面向对象的 Web 浏览器的实现使用了一个过程式的网络库,那么这个程序会混用两种范式。鉴于 在这个应用程序中网络任务的重要性和数量,您可以考虑写一个过程式库的面向对象封装。 例如,假设您正在用 C++编写一个 Web 浏览器,但是您在使用 C 语言的网络库,库中包含以 下代码中声明的函数。请注意,为简洁起见,省略了 HostRecord 和 Connection 数据结构。 // netwrklib.h #include "hostrecord.h" #include "connection.h" // Gets the host record for a particular Internet host given // its hostname (i.e. www.host.com) HostRecord* lookupHostByName(char* inHostName); // Connects to the given host Connection* connectToHost(HostRecord* inHost); // Retrieves a web page from an already-opened connection char* retrieveWebPage(Connection* inConnection, char* page); netwrklib.h 接口相当简单明了。然而,它不是面向对象的,一个使用这样的库的 C++程序员肯 定会觉得很别扭。这个库没有组织成一个内聚的类,甚至没有正确使用 const。当然,一个有才华的 C 程序员可以写一个更好的接口,但作为一个库的用户,您必须接受您所得到的。您有机会编写一 个封装来自定义这个接口。 在我们为这个库构建面向对象的封装之前,看一下这个库是如何使用的,这样才能真正理解使 用的方法。下面这个程序通过 netwrklib 库获取 www.wrox.com/index.html 页面: #include #include "netwrklib.h" using namespace std; int main() { HostRecord* myHostRecord = lookupHostByName("www.wrox.com"); Connection* myConnection = connectToHost(myHostRecord); char* result = retrieveWebPage(myConnection, "/index.html"); cout << "The result is " << result << endl; return 0; } 让这个库变得更面向对象的一种可能方式是在查找主机、连接主机和获取网页之间提供一种识 别链接的单一抽象。一个好的面向对象包装应该隐藏 HostRecord 和 Connection 类型不必要的复杂性。 这个例子遵循了第 3 章和第 4 章描述的设计原则:新的类应该捕捉库的常见用例。前一个例子 展示了最常见的模式—— 首先查找一个主机,然后建立一个连接,最后获取一个页面。同样有可能 需要从同一个主机获取后续页面,因此一个好的设计也要能支持这种使用模式。 786 第 25 章 开发跨平台和跨语言的应用程序 下面是 WebHost 类公共部分的定义。这个类使得客户程序员轻松面对常见情形: // WebHost.h class WebHost { public: // Constructs a WebHost object for the given host WebHost(const string& inHost); // Obtains the given page from this host string getPage(const string& inPage); }; 考虑客户程序员会这样使用这个类: #include #include "WebHost.h" int main() { WebHost myHost("www.wrox.com"); string result = myHost.getPage("/index.html"); cout << "The result is " << result << endl; return 0; } WebHost 类有效地封装了主机的行为,并提供了有用的功能,避免了不必要的调用和数据结构。 这个类甚至提供了一个有用的功能——当创建了一个 WebHost,可以通过这个对象获取多个页面, 节省了代码,还可能使得程序运行得更快。 WebHost 类的实现充分利用了 netwrklib 库,而且没有把这个类的任何内部工作方式向用户公开。 为了实现这个抽象,这个类需要一个数据成员: // WebHost.h #include "netwrklib.h" class WebHost { // Omitted for brevity protected: Connection* mConnection; }; 对应的源文件使得 netwrklib 库中包含的功能面目一新。首先,构造函数为指定的主机构建一个 HostRecord。由于 WebHost 类处理的是 C++ string,而不是 C 风格的字符串,所以对 inHost 使用了 c_str()以便获得一个 const char*,然后执行一个 const 转换,修复了 newwrklib 在 const 方面的错误。 得到的 HostRecord 用于创建一个 Connection,新创建的 Connection 保存在 mConnection 数据成员中 以备后用: WebHost::WebHost(const string& inHost) { const char* host = inHost.c_str(); HostRecord* theHost = lookupHostByName(const_cast(host)); mConnection = connectToHost(theHost); } 787 第Ⅳ部分 C++软件工程 后面对 getPage()的调用将保存的连接传递给 netwrklib 的 retrieveWebPage()函数,然后以 C++ string 的方式返回值: string getPage(const string& inPage) { const char* page = inPage.c_str(); string result = retrieveWebPage(mConnection, const_cast(page)); return result; } 熟悉网络的读者可能会注意到,一直保持打开某个主机的连接是一个不当的行 为,而且没有遵循 HTTP 规范。我们在这个例子中选择了优雅而不是礼仪。 正如您所看到的,WebHost 类为 C 库提供了一个面向对象的封装。通过提供一个抽象,您可以 修改底层实现而不用影响客户代码,您还可以提供额外的特性。这些特性可以包括连接引用计数、 解析页面以及根据 HTTP 规范在特定时间后自动关闭连接并且在下一次 getPage()调用的时候自动重 新打开连接等。 25.2.3 和 C 代码链接 在前面的例子中,我们假设您有可用的原始 C 语言代码。这个例子利用了一个事实,即大部分 C 语言代码都可以通过 C++编译器成功编译。如果您只有编译后的 C 语言代码,可能是库的形式, 那么您仍然可以在 C++程序中使用这些代码,只不过需要一些额外的步骤。 为了实现函数重载,复杂的 C++名称空间被“扁平化”了。例如,如果有一个 C++程序,可以 合法地编写: void MyFunc(double); void MyFunc(int); void MyFunc(int, int); 然而这意味着链接器会看到几个不同的名称,所有名称都称为 MyFunc,不知道您想调用哪一 个。因此,所有 C++编译器都执行了一项名为名称变异(name mangling)的操作,逻辑上等同于生成 了以下名称: MyFunc_double MyFunc_int MyFunc_int_int 为了避免和其他您可能定义的名称冲突,生成的名称通常都会包含一些对链接器合法但是对 C++源代码不合法的字符。例如,Microsoft VC++生成的名称如下: ?MyFunc@@YAXN@Z ?MyFunc@@YAXH@Z ?MyFunc@@YAXHH@Z 这种编码很复杂,而且通常是与特定厂商相关的。C++标准没有规定函数重载应该怎样在指定 平台上实现,因此没有标准的名称变异算法。 在 C 语言中,函数重载是不受支持的(编译器会对重复定义报错)。因此,C 编译器生成的名称 788 第 25 章 开发跨平台和跨语言的应用程序 是非常简单的,例如_MyFunc: 现在,如果您通过 C++编译器编译一个简单的程序,即使这个程序只有一个 MyFunc 名称的实 例,仍然会生成链接变异名称的请求。但是,当您和 C 语言库链接的时候,链接器找不到所需要的 变异的名称,因此链接器会报错。因此,需要告诉 C++编译器不要将名称变异。这是通过在两个头 文件中通过 extern "language"限定(为了指导客户代码创建一个和指定语言兼容的名称)的方式实现 的,而且如果您的库源代码是用 C++写的,还需要在定义的地方加上(为了指导库代码生成一个和指 定语言兼容的名称)。 extern "language" 的语法如下所示: extern "language" declaration1(); extern "language" declaration2(); 或: extern "language" { declaration1(); declaration2(); } C++标准指出可以使用任何语言规范,所以编译器原则上能支持以下代码: extern "C" MyFunc(int i); extern "FORTRAN" MatrixInvert(Matrix* M); extern "Pascal" SomeLegacySubroutine(int n); extern "Ada" AimMissileDefense(double angle); 在实际中,许多编译器只支持"C"。每一个编译器厂商都会告诉您支持哪一些语言标记。 例如,在下面的代码中,doCFunction()的函数原型指定为一个外部 C 函数: extern "C" { void doCFunction(int i); } int main() { doCFunction(8); // Call the C function. return 0; } doCFunction()函数的实际定义在一个编译好的二进制文件中,链接的时候附上这个文件。extern 关键字告诉编译器,链入的代码是使用 C 语言编译的。 使用 extern 的一个更常见的模式是在头文件的层次使用。例如,如果您在使用一个用 C 语言编 写的图形库,这个库可能带有一个供您使用的.h 文件。您可以编写另一个头文件,将原来的头文件 包含在 extern 块中,表示整个头文件定义的函数都是用 C 编写的。包装的.h 文件通常以.hpp 命名, 以便和 C 版本的头文件区分: // graphicslib.hpp extern "C" { #include "graphicslib.h" } 789 第Ⅳ部分 C++软件工程 另一个常见模型是单独写一个头文件,并且通过条件编译选择 C 或 C++的部分。如果正在为 C++编译,C++编译器会预定义一个符号__cplusplus。编译 C 的时候没有定义这个符号。所以您经 常会看到以下形式的头文件: #ifdef __cplusplus extern "C" { #endif declaration1(); declaration2(); #ifdef __cplusplus } // matches extern "C" #endif 这意味着 declaration1()和 declaration2()是 C 语言编译器编译的库中的函数。使用这种技术,C 语言和 C++的客户可以使用同一个头文件。 不论您是在 C++程序中包含 C 代码,还是链接 C 语言编译的库,要记住,尽管 C++是 C 语言 的超集,这两种语言实际上是设计目标不同的不同语言。在 C++中采用 C 语言代码是很常见的,但 是给过程式的 C 语言代码提供面向对象的 C++封装是更好的选择。 25.2.4 混合使用 C#与 C++ 尽管这是一本 C++的书,但是我们不会假装没有更新更时髦的语言。C#是一个例子。通过使用 C#中的 Interop 服务,在 C#应用程序中调用 C++代码非常容易。一个示例场景可能是您正在用 C# 开发应用程序的一部分,例如图形用户界面,但是用 C++实现一些性能关键的组件。为了使得 Interop 工作,您需要用 C++编写一个库,然后在 C#中调用这个库。在 Windows 上,这个库在.DLL 文件中。 下面的 C++例子定义了一个 FunctionInDLL()函数,这个函数将被编译成库。该函数接受一个 Unicode 字符串,并返回一个整数。这个实现将接收到的字符串写入控制台,并向调用者返回值 42: #include using namespace std; extern "C" { __declspec(dllexport) int FunctionInDLL(const wchar_t* p) { wcout << L"The following string was received by C++:\n '"; wcout << p << L"'" << endl; return 42; // Return some value... } } 代码取自 CSharp\HelloCpp.cpp 请记住,您正在实现一个库中的函数,而不是在编写一个程序,所以不需要一个 main()函数。 这段代码的编译取决于您的环境。如果您正在使用 Microsoft Visual C++,需要进入项目属性,然后 选择 Dynamic Library (.dll)作为配置类型。请注意,这个例子通过__declspec(dllexport)告诉链接器这 个函数应该让库的客户使用。具体的实现取决于您的编译器。 __declspec(dllexport)的方式是 Microsoft Visual C++使用的方式。 790 第 25 章 开发跨平台和跨语言的应用程序 有了这个库之后,就可以在 C#中通过 Interop 服务进行调用。首先,需要包含 Interop 的名称 空间: using System.Runtime.InteropServices; 接下来,定义函数原型,并告诉 C#在哪里可以找到这个函数的实现。这是通过下面这行代码实 现的,假设已经将这个库编译为 HellpCpp.dll: [DllImport("HelloCpp.dll", CharSet = CharSet.Unicode)] public static extern int FunctionInDLL(String s); 这行的第一部分表示 C#应该从一个名为 HelloCpp.dll 的库导入这个函数,而且应该使用 Unicode 字符串。第二部分说明了这个函数的实际原型,表示这是一个接受一个字符串作为参数并返回一个 整数的函数。下面的代码展示了如何在 C#中使用 C++库的完整例子: using System; using System.Runtime.InteropServices; namespace HelloCSharp { class Program { [DllImport("HelloCpp.dll", CharSet = CharSet.Unicode)] public static extern int FunctionInDLL(String s); static void Main(string[] args) { Console.WriteLine("Writen by C#."); int res = FunctionInDLL("Some string from C#."); Console.WriteLine("C++ returned the value " + res); } } } 代码取自 CSharp\HelloCSharp.cs 输出如下所示: Writen by C#. The following string was received by C++: 'Some string from C#.' C++ returned the value 42 这段 C#代码的细节已经超出了这本 C++书的范围,不过通过这个例子应该了解了总体思路。 25.2.5 通过 JNI 混合 Java 和 C++ Java Native Interface(Java 原生接口,JNI)是 Java 语言的一部分,允许程序员访问非 Java 语言编 写的功能。由于 Java 是一种跨平台的语言,这个接口的初衷是让 Java 程序和操作系统交互。JNI 也 允许程序员使用其他语言编写的库,如 C++。如果 Java 程序员在应用程序中有性能关键的部分,或 者需要使用遗留代码,那么访问 C++库的能力对 Java 程序员来说是非常有用的。 JNI 也可以用于在 C++程序中执行 Java 代码,但是这种用法很少见。因为这是一本 C++的书, 所以我们不会介绍 Java 语言。本节面向应该已经知道 Java,而且想在 Java 代码中结合 C++代码的 791 第Ⅳ部分 C++软件工程 读者。 下面以一个 Java 程序开始 Java 的跨语言之旅。对于这个例子,最简单的 Java 程序就足够了: public class HelloCpp { public static void main(String[] args) { System.out.println("Hello from Java!"); } } 代码取自 JNI\HelloCpp.java 接下来,需要声明一个用其他语言编写的 Java 方法。要做到这一点,使用 native 关键字并且不 给出实现: public class HelloCpp { // This will be implemented in C++. public native void callCpp(); // Remainder omitted for brevity } 代码取自 JNI\HelloCpp.java C++代码最终会被编译成一个共享库被动态加载到 Java 程序。您需要在一个 Java 静态块中加载 这个库,这样才能在 Java 程序开始执行时加载这个库。您可以任意命名这个库,例如 Unix 系统上 的 hellocpp.so 或 Windows 系统上的 hellocpp.dll。 public class HelloCpp { static { System.loadLibrary("hellocpp"); } // Remainder omitted for brevity } 代码取自 JNI\HelloCpp.java 最后,在 Java 程序中实际调用这段 C++代码。callCpp() Java 方法是一个占位符,表示尚未编写 的 C++代码。由于 callCpp()是 HelloCpp 类的一个方法,需要创建一个新的 HelloCpp 对象并且调用 这个对象的 callCpp()方法: public class HelloCpp { static { System.loadLibrary("hellocpp"); } // This will be implemented in C++. public native void callCpp(); public static void main(String[] args) { System.out.println("Hello from Java!"); HelloCpp cppInterface = new HelloCpp(); cppInterface.callCpp(); } 792 第 25 章 开发跨平台和跨语言的应用程序 } 代码取自 JNI\HelloCpp.java 这是 Java 这边所有的内容。现在,只需像往常一样编译 Java 程序: javac HelloCpp.java 然后使用 javah(作者喜欢把这个程序读成 jav-AHH!)程序为这个原始方法创建一个头文件: javah HelloCpp javah 运行完毕后,您会发现一个名为 HelloCpp.h 的文件,这是一个可以完全正常工作(只不过 有点丑陋)的 C/C++头文件。在这个头文件内是一个名为 Java_HelloCpp_callCpp()的函数的 C 函数定 义。您的 C + +程序需要实现这个功能。完整原型如下: JNIEXPORT void JNICALL Java_HelloCpp_callCpp(JNIEnv* env, jobject javaobj); 这个函数的 C++实现可以充分利用 C++语言的特性。这个例子通过 C++输出一些文本。首先, 您需要包含 jni.h 头文件和 javah 创建的 HellpCpp.h 文件。您还需要包括任何您需要使用的 C 或 C++ 头文件: #include #include "HelloCpp.h" #include 代码取自 JNI\HelloCpp.cpp 按照正常的方式编写 C++函数。这个函数的参数允许和 Java 环境以及调用这个原生代码的对象 交互。这些内容超出了这个例子的范围。 JNIEXPORT void JNICALL Java_HelloCpp_callCpp(JNIEnv* env, jobject javaobj) { std::cout << "Hello from C++!" << std::endl; } 代码取自 JNI\HelloCpp.cpp 将这段代码编译为一个库取决于您的环境,但您肯定要调整一下编译器的设置以包含 JNI 头文 件。如果在 Linux 上使用 GCC 编译器,编译命令可能看起来像这样: g++ -shared -I/usr/java/jdk/include/ -I/usr/java/jdk/include/linux HelloCpp.cpp \ -o hellocpp.so 从编译器的输出是由 Java 程序使用的库。只要这个共享库在 Java 类路径的某个位置,您就可 以正常执行 Java 程序: java HelloCpp 应该可以看到以下结果: Hello from Java! 793 第Ⅳ部分 C++软件工程 Hello from C++! 当然,这个例子只介绍了 JNI 的冰山一角。您可以通过 JNI 与操作系统相关的特性或硬件驱动 程序交互。有关 JNI 的完整内容,请参阅 Java 教材。 25.2.6 混合 C++使用 Perl 和 shell 脚本 C++包含一个内建的通用机制可以和其他语言和环境接口。您已经使用这种机制很多次了,但 是可能没有太注意——这就是 main()函数的参数和返回值。 C 和 C++在设计的时候考虑的是命令行接口。main()函数从命令行接收参数,并且返回一个状 态码供调用者解释。在脚本环境中,参数和程序返回的状态码是与环境交互的强大机制。 1. 脚本还是编程 在深入学习 C++和脚本语言混合编程之前,首先考虑您的项目是应用程序还是一个脚本。两者 之间的区别很微妙,而且存在争论。下面的说明只是指导方针。许多所谓的脚本和成熟的应用程序 一样复杂。问题不在于某件事情是否能通过脚本完成,而是脚本语言是否是最合适的工具。 应用程序是完成某一项特定任务的程序。现代应用程序通常涉及某种用户交互。换句话说,应 用程序往往由用户驱动,用户指导应用程序应该采取什么操作。应用程序通常具有多种功能。例如, 用户可以利用照片编辑应用程序缩放图像、在图像上绘制或打印图像。大部分盒装软件都是一个应 用程序。应用程序通常比较大,而且往往是复杂的程序。 脚本通常执行一个任务,或一组相关的任务。您可以通过一个脚本程序自动分拣电子邮件或备 份重要文件。脚本通常在没有用户交互的情况下运行,也许是在每天的一个特定时间运行或由事件 引发,如新邮件到达。脚本可以在操作系统层次(例如每天晚上压缩文件的脚本),或在应用程序的 层次(例如一个自动压缩和打印图像的脚本)。自动化是脚本定义中的一个重要部分—— 编写脚本的目 的通常是用代码描述一些用户可以人工完成的步骤。 现在,考虑脚本语言和编程语言之间的差异。并非所有脚本都必须用脚本语言编写。您可以用 C 语言编写一个分拣电子邮件的脚本,还可以用 Perl 脚本语言编写一个相同的程序。同样,并不是 所有的应用程序都是用编程语言编写的。一个有正当动机的程序员可以根据自己的需要用 Perl 语言 编写一个 Web 浏览器。界线是模糊的。 最后,最重要的是哪种语言提供了您所需要的功能。如果您需要和操作系统做大量的交互,您 可以考虑使用脚本语言,因为脚本语言对操作系统交互的支持更好。如果您的项目应用范围更广, 而且涉及大量用户交互,那么长久来看,编程语言是更好的选择。 2. 使用脚本 原始的 Unix 操作系统包括一个相当有限的 C 库,这个库不支持一些常用的操作。因此 Unix 程 序员养成了从应用程序中加载 shell 脚本的习惯,以完成某些 API 或库不支持的任务。 如今,很多这些 Unix 程序员仍然习惯使用脚本作为一种子例程调用的形式。他们通常执行 C 库函数 system(),通过一个字符串表示要执行的脚本。这种方法也有重大风险。例如,如果脚本有 一个错误,调用者可能会也可能不会得到一个详细的错误提示。system()调用的量级很重,因为这个 调用要创建一个新的进程执行脚本。这最终可能成为应用程序中的一个严重性能瓶颈。 通常情况下,您应该探寻 C++库中的特性,看看有没有更适合某个任务的特性。有很多平台无 794 第 25 章 开发跨平台和跨语言的应用程序 关的包装库封装了很多平台相关的库,例如 Boost 库。通过 system()启动 Perl 脚本来处 理一些文本数据之类的做法可能不是最好的选择。使用 C++11 中正则表达式这类技术可能是字符串 处理更好的选择。 3. 一个实际的例子:密码加密 假设您有这样一个系统,将用户看到的和输入的所有内容写入文件用作审核的目的。这个文件 只有系统管理员才能读取,这样出现了错误之后才能找出是谁的错误。这个文件的摘录看起来像 这样: Login: bucky-bo Password: feldspar bucky-bo> mail bucky-bo has no mail bucky-bo> exit 虽然系统管理员可能希望保存所有用户活动的日志,但是她可能想要掩盖所有用户的密码,以 防这个文件被黑客获取。脚本似乎是这个项目的自然之选,因为这个任务应该自动进行,例如在每 天结束的时候。然而,这个项目中有一个部分可能不是最适合脚本语言的。加密库常常存在于高级 语言中,例如 C 和 C++。因此,一种可能的实现是编写一个脚本调用 C++程序执行加密。 下面的脚本使用 Perl 语言,虽然几乎所有的脚本语言可以完成这项任务。如果您不知道 Perl, 您还是能看懂下面的内容。在这个例子中 Perl 语法最重要的元素是`字符。`字符告诉 Perl 脚本执行 一个外部命令。在这个例子中,这个脚本将调用一个名为 encryptString 的 C++程序。 启动一个外部进程会造成很大的开销,因为要建立一个完整的新进程。如果需要 频繁调用外部进程则不应该使用外部调用。在这个密码加密的例子中问题不大,因为 假设一个日志文件中包含的密码行并不多。 这个脚本采用的策略是循环遍历文件的每一行,寻找包含提示密码的行。该脚本写入一个新的 文件 userlog.out,这个文件的内容和源文件的内容一致,只不过其中的密码都经过了加密。第一步 是打开供读取的输入文件和供写入的输出文件。然后这个脚本要遍历文件中的所有行。接下来每一 行都放在一个名为$line 的变量中。 open (INPUT, "userlog.txt") or die "Couldn't open input file!"; open (OUTPUT, ">userlog.out") or die "Couldn't open output file!"; while ($line = ) { 代码取自 Perl\processLog.pl 然后,通过正则表达式检测当前行是否包含 Password:提示符。如果包含,Perl 会将密码保存在 变量$1 中。 if ($line =~ m/^Password: (.*)/) { 由于找到了匹配,脚本调用 encryptString 程序,通过检测到的密码获得一个加密的版本。程序 795 第Ⅳ部分 C++软件工程 的输出存储保存在$result 变量中,从程序的结果状态代码保存在变量$?中。脚本检查$?变量,如果 出现问题则立即退出。如果一切正常,密码行被写入输出文件,并且用加密的密码替代原来的密码。 $result = `./encryptString $1`; if ($? != 0) { exit(-1) } print OUTPUT "Password: $result\n"; 如果当前行不是一个密码提示符,脚本则将当前行原样写入输出文件。在循环结束时,脚本关 闭这两个文件并退出。 } else { print OUTPUT "$line"; } } close (INPUT); close (OUTPUT); 就是这样。项目中需要的另一部分是实际的 C++程序。加密算法的实现超出了本书的讨论范围。 最重要的部分是 main()函数,因为这个函数接受一个需要加密的字符串作为参数。 参数都包含在 C 风格字符串数组 argc 中。在访问 argv 的元素之前,总是应该查看 argc 参数。 如果 argc 等于 1,那么参数列表中只有 1 个元素,可以通过 argv[0]访问。argv 数组的第 0 个元素通 常是程序的名称,因此实际的参数从 argv[1]开始。 下面是这个加密输入字符串的 C++程序的 main()函数。注意,这个程序返回 0 表示成功,返回 非 0 值表示失败,这是 Unix 中使用的标准。 int main(int argc, char* argv[]) { if (argc < 2) { cerr << "Usage: " << argv[0] << " string-to-be-encrypted" << endl; return -1; } cout << encrypt(argv[1]); return 0; } 代码取自 Perl\encryptString.cpp 在这个代码中实际上有一个明显漏洞。要加密的字符串是作为命令行参数传递给 这个 C++程序的,因此其他用户可以通过进程表查看到这个参数。C++程序获得输入 的一个更安全的方法是通过标准输入向这个程序发送数据,这也是脚本语言的长处。 现在您已经了解了 C++程序可以很简单地和脚本语言结合在一起,您可以在自己的项目中整合 两种语言的强大之处。您可以通过脚本语言和操作系统交互,并且控制脚本的流程,另外利用传统 编程语言完成繁重的任务。 796 第 25 章 开发跨平台和跨语言的应用程序 这个例子只是为了演示如何结合使用 Perl 和 C++。C++11 包含了一个正则表达式 库,因此很容易将这个 Perl/C++的解决方案转换为一个纯 C++的解决方案。纯 C++解 决方案将运行得更快,因为它避免了调用外部程序。有关正则表达式库的详细信息, 请参阅第 14 章。 25.2.7 混合使用 C++和汇编代码 C++被认为是一种速度很快的语言,特别是和其他面向对象语言比较的时候。然而,在某些罕 见的情况下,当速度至关重要的时候,您可能需要使用原始的汇编代码。编译器从您的源代码文件 生成汇编代码,这个生成的汇编代码几乎对于所有的目的都足够快。编译器和链接器(当像 VC++ 2010 那样支持链接时代码生成的时候)都使用了优化算法使得生成的汇编代码能够尽可能地快。通 过使用特殊的处理器指令集,例如 MMX 和 SSE,这些优化器变得越来越强大。如今,自己编写的 汇编代码很难比编译器生成的代码速度更快,除非您了解这些增强指令集的所有细节。 然而,万一您需要使用汇编代码,在 C++编译器中可以使用 asm 关键字插入原始汇编代码。这 个关键字是 C++标准的一部分,但是实现方式却是由编译器自己定义的。在有些编译器中,您可以 通过 asm 关键字在程序中立即从 C++降到汇编层次。有时,asm 关键字的支持取决于您的目标架构。 例如,编译 32 位代码的时候 Microsoft VC++ 2010 支持 asm 关键字,但是编译 64 位模式的时候不 支持 asm。 在某些应用程序中内联汇编会有用,但是我们对大部分程序都不建议使用。应该避免内联汇编 代码,原因有以下几点: ● 如果包含了针对您的平台的汇编代码,那么您的代码将不能移植到其他处理器。 ● 大多数程序员不知道汇编语言,他们无法修改或维护您的代码。 ● 汇编代码的可读性差。汇编代码可能会破坏您的程序使用的风格。 ● 大部分时候,汇编代码是没有必要的。如果您的程序缓慢,检查算法问题或者采用第 24 章 给出的其他性能建议。 提示:当您的应用程序遇到性能问题的时候,首先查看算法加速,原始汇编只是 万不得已才考虑使用的解决方案。 实际上,如果您有一块代码计算量非常大,那么您应该把这段代码放在一个 C++函数中。如果 通过性能剖析(详见第 24 章)判定这函数是一个性能瓶颈,而且无法使得这段代码更简单更快,那么 可以通过原始汇编尝试提高性能。 在这种情况下,您需要以 extern "C"声明这个函数,这样可以避免 C++的名称变异。然后,用 汇编代码写一个单独的模块更高效地执行这个函数。一个单独的模块的好处是,有一个平台无关的 C++“参考实现”;同时还有一个平台相关的高性能汇编代码实现。使用 extern "C"意味着这段汇编 代码可以使用简单的命名约定(否则,您不得不逆向工程编译器的名称变异算法)。然后,可以和 C++ 版本链接,也可以和汇编代码的版本链接。 797 第Ⅳ部分 C++软件工程 您可以用汇编代码编写这个模块并通过汇编器运行这段代码,而不是使用 C++中的内联 asm 指 令;对于很多流行的 x86 兼容的 64 位编译器来说尤其如此,因为这些编译器不支持内联的 asm 关 键字。 不过,除非确实有显著的性能提升,否则不要使用原始汇编代码。2 倍的性能提升可以证明这 个努力是有效的。10 倍的提升是令人信服的。10%的提升则得不偿失。 25.3 本章小结 通过本章的学习,您至少应该知道 C++是一门灵活的语言。C++既不是一个和特定平台绑定太 紧密的语言,也不是太高级太通用的语言,而是在两者之间的甜蜜点。当您在用 C++开发代码的时 候可以放松,因为您没有永远被锁定在这个语言上。C++可以和其他技术混合使用,而且有着坚实 的历史和代码基础,从而有助于保证其在未来的关联性。 798
更多简介内容

评论

下载专区


TI最新应用解决方案

工业电子 汽车电子 个人消费电子

$(function(){ var appid = $(".select li a").data("channel"); $(".select li a").click(function(){ var appid = $(this).data("channel"); $('.select dt').html($(this).html()); $('#channel').val(appid); }) })